From e56a37170b017c3bade81eedab57a99a5e2f019f Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Sun, 17 Aug 2014 14:38:14 +0100 Subject: [PATCH 1/8] autoInject - dependency injection for auto. --- README.md | 68 +++++++++++++++++++ lib/async.js | 34 ++++++++++ test/test-async.js | 165 ++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 265 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8f4e98c03..60ed04a51 100644 --- a/README.md +++ b/README.md @@ -147,6 +147,7 @@ Usage: * [`priorityQueue`](#priorityQueue) * [`cargo`](#cargo) * [`auto`](#auto) +* [`autoInject`](#autoInject) * [`retry`](#retry) * [`iterator`](#iterator) * [`apply`](#apply) @@ -1342,6 +1343,73 @@ For a complicated series of `async` tasks, using the [`auto`](#auto) function ma new tasks much easier (and the code more readable). +--------------------------------------- + + +### autoInject(tasks, [callback]) + +A dependency-injected version of the [`auto`](#auto) function. Dependent tasks are +specified as parameters to the function, after the usual callback parameter, with the +parameter names matching the names of the tasks it depends on. This can provide even more +readable task graphs which can be easier to maintain. + +If a final callback is specified, the task results are similarly injected, specified as +named parameters after the initial error parameter. + +The autoInject function is purely syntactic sugar and its semantics are otherwise +equivalent to [`auto`](#auto). + +__Arguments__ + +* `tasks` - An object, each of whose properties is a function of the form + 'func(callback, [dependencies...]). The object's key of a property serves as the + name of the task defined by that property, i.e. can be used when specifying requirements + for other tasks. + The `callback` parameter is a `callback(err, result)` which must be called when finished, + passing an `error` (which can be `null`) and the result of the function's execution. + The remaining parameters name other tasks on which the task is dependent, and the results + from those tasks are the arguments of those parameters. +* `callback(err, [results...])` - An optional callback which is called when all the + tasks have been completed. It receives the `err` argument if any `tasks` + pass an error to their callback. The remaining parameters are task names whose results + you are interested in. This callback will only be called when all tasks have finished or + an error has occurred, and so do not not specify dependencies in the same way as `tasks` + do. If an error occurs, no further `tasks` will be performed, and `results` will only be + valid for those tasks which managed to complete. + + +__Example__ + +The example from [`auto`](#auto) can be rewritten as follows: + +```js +async.autoInject({ + get_data: function(callback){ + // async code to get some data + callback(null, 'data', 'converted to array'); + }, + make_folder: function(callback){ + // async code to create a directory to store a file in + // this is run at the same time as getting the data + callback(null, 'folder'); + }, + write_file: function(callback, get_data, make_folder){ + // once there is some data and the directory exists, + // write the data to a file in the directory + callback(null, 'filename'); + }, + email_link: function(callback, write_file){ + // once the file is written let's email a link to it... + // write_file contains the filename returned by write_file. + callback(null, {'file':write_file, 'email':'user@example.com'}); + } +}, function(err, email_link) { + console.log('err = ', err); + console.log('email_link = ', email_link); +}); +``` + + --------------------------------------- diff --git a/lib/async.js b/lib/async.js index a13f83520..4d2d69f28 100755 --- a/lib/async.js +++ b/lib/async.js @@ -498,6 +498,40 @@ }); }; + var _argsRegEx = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; + var _diArgs = function(func) { + var result = func.toString().match(_argsRegEx)[1].split(','); + for (var i = 0, e = result.length; i != e; ++i) + result[i] = result[i].trim(); + return result; + }; + + async.autoInject = function(tasks, callback) { + var injTasks = {}; + for (var i in tasks) + injTasks[i] = (function(val) { + if (!(val instanceof Function)) + return val; + + var args = _diArgs(val); + if (args.length < 2) + return val; + + args.shift(); + + return args.concat(function(cb, results) { + val.apply(null, [cb].concat(_map(args, function(a) { return results[a]; }))); + }); + })(tasks[i]); + + return this.auto( + injTasks, + callback && function(err, results) { + callback.apply(null, [err].concat(_map(_diArgs(callback).slice(1), function(a) { return results[a]; }))); + } + ); + }; + async.retry = function(times, task, callback) { var DEFAULT_TIMES = 5; var attempts = []; diff --git a/test/test-async.js b/test/test-async.js index 6a3560694..81801199c 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -454,7 +454,6 @@ exports['auto results'] = function(test){ }); }; - exports['auto empty object'] = function(test){ async.auto({}, function(err){ test.done(); @@ -504,7 +503,169 @@ exports['auto error should pass partial results'] = function(test) { test.equals(err, 'testerror'); test.equals(results.task1, 'result1'); test.equals(results.task2, 'result2'); - test.done(); + test.done(); + }); +}; + +exports['autoInject'] = function(test){ + var callOrder = []; + var testdata = [{test: 'test'}]; + async.autoInject({ + task1: function(callback, task2){ + setTimeout(function(){ + callOrder.push('task1'); + callback(); + }, 25); + }, + task2: function(callback){ + setTimeout(function(){ + callOrder.push('task2'); + callback(); + }, 50); + }, + task3: function(callback, task2){ + callOrder.push('task3'); + callback(); + }, + task4: function(callback, task1, task2){ + callOrder.push('task4'); + callback(); + }, + task5: function(callback, task2){ + setTimeout(function(){ + callOrder.push('task5'); + callback(); + }, 0); + }, + task6: function(callback, task2){ + callOrder.push('task6'); + callback(); + } + }, + function(err){ + test.same(callOrder, ['task2','task6','task3','task5','task1','task4']); + test.done(); + }); +}; + +exports['autoInject petrify'] = function (test) { + var callOrder = []; + async.autoInject({ + task1: function (callback, task2) { + setTimeout(function () { + callOrder.push('task1'); + callback(); + }, 100); + }, + task2: function (callback) { + setTimeout(function () { + callOrder.push('task2'); + callback(); + }, 200); + }, + task3: function (callback, task2) { + callOrder.push('task3'); + callback(); + }, + task4: function (callback, task1, task2) { + callOrder.push('task4'); + callback(); + } + }, + function (err) { + test.same(callOrder, ['task2', 'task3', 'task1', 'task4']); + test.done(); + }); +}; + +exports['autoInject results'] = function(test){ + var callOrder = []; + async.autoInject({ + task1: function(callback, task2){ + test.same(task2, 'task2'); + setTimeout(function(){ + callOrder.push('task1'); + callback(null, 'task1a', 'task1b'); + }, 25); + }, + task2: function(callback){ + setTimeout(function(){ + callOrder.push('task2'); + callback(null, 'task2'); + }, 50); + }, + task3: function(callback, task2){ + test.same(task2, 'task2'); + callOrder.push('task3'); + callback(null); + }, + task4: function(callback, task1, task2){ + test.same(task1, ['task1a','task1b']); + test.same(task2, 'task2'); + callOrder.push('task4'); + callback(null, 'task4'); + } + }, + function(err, task2, task4, task3, task1){ + test.same(callOrder, ['task2','task3','task1','task4']); + test.same(task1, ['task1a','task1b']); + test.same(task2, 'task2'); + test.same(task3, undefined); + test.same(task4, 'task4'); + test.done(); + }); +}; + +exports['autoInject empty object'] = function(test){ + async.autoInject({}, function(err){ + test.done(); + }); +}; + +exports['autoInject error'] = function(test){ + test.expect(1); + async.autoInject({ + task1: function(callback){ + callback('testerror'); + }, + task2: function(callback, task1){ + test.ok(false, 'task2 should not be called'); + callback(); + }, + task3: function(callback){ + callback('testerror2'); + } + }, + function(err){ + test.equals(err, 'testerror'); + }); + setTimeout(test.done, 100); +}; + +exports['autoInject no callback'] = function(test){ + async.autoInject({ + task1: function(callback){callback();}, + task2: function(callback, task1){callback(); test.done();} + }); +}; + +exports['autoInject error should pass partial results'] = function(test) { + async.autoInject({ + task1: function(callback){ + callback(false, 'result1'); + }, + task2: function(callback, task1){ + callback('testerror', 'result2'); + }, + task3: function(callback, task2){ + test.ok(false, 'task3 should not be called'); + } + }, + function(err, task1, task2){ + test.equals(err, 'testerror'); + test.equals(task1, 'result1'); + test.equals(task2, 'result2'); + test.done(); }); }; From 99920c3f73be88a66046ec953ee896d1506c72e6 Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Sun, 6 Mar 2016 11:00:23 +0000 Subject: [PATCH 2/8] Callback goes at the end. --- lib/async.js | 32 ++++++++++++++++++++------------ test/test-async.js | 30 +++++++++++++++--------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/lib/async.js b/lib/async.js index e38c901c9..dbdb090c5 100755 --- a/lib/async.js +++ b/lib/async.js @@ -608,36 +608,44 @@ }); }; - var _argsRegEx = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; - var _diArgs = function(func) { - var result = func.toString().match(_argsRegEx)[1].split(','); - for (var i = 0, e = result.length; i != e; ++i) + var _funcParamRegEx = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; + var _funcParamNames = function(func) { + var result = func.toString().match(_funcParamRegEx)[1].split(','); + + for (var i = 0, e = result.length; i != e; ++i) { result[i] = result[i].trim(); + } + return result; }; async.autoInject = function(tasks, callback) { var injTasks = {}; - for (var i in tasks) + for (var i in tasks) { injTasks[i] = (function(val) { - if (!(val instanceof Function)) + if (!(val instanceof Function)) { return val; + } - var args = _diArgs(val); - if (args.length < 2) + var params = _funcParamNames(val); + if (params.length < 2) { return val; + } - args.shift(); + params.pop(); - return args.concat(function(cb, results) { - val.apply(null, [cb].concat(_map(args, function(a) { return results[a]; }))); + return params.concat(function(cb, results) { + val.apply(null, _map(params, function(a) { return results[a]; }).concat(cb)); }); + + return params; })(tasks[i]); + } return this.auto( injTasks, callback && function(err, results) { - callback.apply(null, [err].concat(_map(_diArgs(callback).slice(1), function(a) { return results[a]; }))); + callback.apply(null, [err].concat(_map(_funcParamNames(callback).slice(1), function(a) { return results[a]; }))); } ); }; diff --git a/test/test-async.js b/test/test-async.js index bead45b1d..8133a1c3c 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -491,7 +491,7 @@ exports['autoInject'] = function(test){ var callOrder = []; var testdata = [{test: 'test'}]; async.autoInject({ - task1: function(callback, task2){ + task1: function(task2, callback){ setTimeout(function(){ callOrder.push('task1'); callback(); @@ -503,21 +503,21 @@ exports['autoInject'] = function(test){ callback(); }, 50); }, - task3: function(callback, task2){ + task3: function(task2, callback){ callOrder.push('task3'); callback(); }, - task4: function(callback, task1, task2){ + task4: function(task1, task2, callback){ callOrder.push('task4'); callback(); }, - task5: function(callback, task2){ + task5: function(task2, callback){ setTimeout(function(){ callOrder.push('task5'); callback(); }, 0); }, - task6: function(callback, task2){ + task6: function(task2, callback){ callOrder.push('task6'); callback(); } @@ -531,7 +531,7 @@ exports['autoInject'] = function(test){ exports['autoInject petrify'] = function (test) { var callOrder = []; async.autoInject({ - task1: function (callback, task2) { + task1: function (task2, callback) { setTimeout(function () { callOrder.push('task1'); callback(); @@ -543,11 +543,11 @@ exports['autoInject petrify'] = function (test) { callback(); }, 200); }, - task3: function (callback, task2) { + task3: function (task2, callback) { callOrder.push('task3'); callback(); }, - task4: function (callback, task1, task2) { + task4: function (task1, task2, callback) { callOrder.push('task4'); callback(); } @@ -561,7 +561,7 @@ exports['autoInject petrify'] = function (test) { exports['autoInject results'] = function(test){ var callOrder = []; async.autoInject({ - task1: function(callback, task2){ + task1: function(task2, callback){ test.same(task2, 'task2'); setTimeout(function(){ callOrder.push('task1'); @@ -574,12 +574,12 @@ exports['autoInject results'] = function(test){ callback(null, 'task2'); }, 50); }, - task3: function(callback, task2){ + task3: function(task2, callback){ test.same(task2, 'task2'); callOrder.push('task3'); callback(null); }, - task4: function(callback, task1, task2){ + task4: function(task1, task2, callback){ test.same(task1, ['task1a','task1b']); test.same(task2, 'task2'); callOrder.push('task4'); @@ -608,7 +608,7 @@ exports['autoInject error'] = function(test){ task1: function(callback){ callback('testerror'); }, - task2: function(callback, task1){ + task2: function(task1, callback){ test.ok(false, 'task2 should not be called'); callback(); }, @@ -625,7 +625,7 @@ exports['autoInject error'] = function(test){ exports['autoInject no callback'] = function(test){ async.autoInject({ task1: function(callback){callback();}, - task2: function(callback, task1){callback(); test.done();} + task2: function(task1, callback){callback(); test.done();} }); }; @@ -634,10 +634,10 @@ exports['autoInject error should pass partial results'] = function(test) { task1: function(callback){ callback(false, 'result1'); }, - task2: function(callback, task1){ + task2: function(task1, callback){ callback('testerror', 'result2'); }, - task3: function(callback, task2){ + task3: function(task2, callback){ test.ok(false, 'task3 should not be called'); } }, From 828231b16f339dbeaf4559ea692a592cc4566f50 Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Sun, 6 Mar 2016 16:46:12 +0000 Subject: [PATCH 3/8] autoInject tests updated. --- test/test-async.js | 238 ++++++++++++++++++++++++++++++++------------- 1 file changed, 168 insertions(+), 70 deletions(-) diff --git a/test/test-async.js b/test/test-async.js index 8133a1c3c..325333624 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -487,9 +487,138 @@ exports['auto error should pass partial results'] = function(test) { }); }; +// Issue 24 on github: https://github.com/caolan/async/issues#issue/24 +// Issue 76 on github: https://github.com/caolan/async/issues#issue/76 +exports['auto removeListener has side effect on loop iterator'] = function(test) { + async.auto({ + task1: ['task3', function(/*callback*/) { test.done(); }], + task2: ['task3', function(/*callback*/) { /* by design: DON'T call callback */ }], + task3: function(callback) { callback(); } + }); +}; + +// Issue 410 on github: https://github.com/caolan/async/issues/410 +exports['auto calls callback multiple times'] = function(test) { + if (isBrowser()) { + // node only test + test.done(); + return; + } + var finalCallCount = 0; + var domain = require('domain').create(); + domain.on('error', function (e) { + // ignore test error + if (!e._test_error) { + return test.done(e); + } + }); + domain.run(function () { + async.auto({ + task1: function(callback) { callback(null); }, + task2: ['task1', function(callback) { callback(null); }] + }, + + // Error throwing final callback. This should only run once + function() { + finalCallCount++; + var e = new Error("An error"); + e._test_error = true; + throw e; + }); + }); + setTimeout(function () { + test.equal(finalCallCount, 1, + "Final auto callback should only be called once" + ); + test.done(); + }, 10); +}; + + +exports['auto calls callback multiple times with parallel functions'] = function(test) { + test.expect(1); + async.auto({ + task1: function(callback) { setTimeout(callback,0,"err"); }, + task2: function(callback) { setTimeout(callback,0,"err"); } + }, + // Error throwing final callback. This should only run once + function(err) { + test.equal(err, "err"); + test.done(); + }); +}; + + +// Issue 462 on github: https://github.com/caolan/async/issues/462 +exports['auto modifying results causes final callback to run early'] = function(test) { + async.auto({ + task1: function(callback, results){ + results.inserted = true; + callback(null, 'task1'); + }, + task2: function(callback){ + setTimeout(function(){ + callback(null, 'task2'); + }, 50); + }, + task3: function(callback){ + setTimeout(function(){ + callback(null, 'task3'); + }, 100); + } + }, + function(err, results){ + test.equal(results.inserted, true); + test.ok(results.task3, 'task3'); + test.done(); + }); +}; + +// Issue 263 on github: https://github.com/caolan/async/issues/263 +exports['auto prevent dead-locks due to inexistant dependencies'] = function(test) { + test.throws(function () { + async.auto({ + task1: ['noexist', function(callback){ + callback(null, 'task1'); + }] + }); + }, Error); + test.done(); +}; + +// Issue 263 on github: https://github.com/caolan/async/issues/263 +exports['auto prevent dead-locks due to cyclic dependencies'] = function(test) { + test.throws(function () { + async.auto({ + task1: ['task2', function(callback){ + callback(null, 'task1'); + }], + task2: ['task1', function(callback){ + callback(null, 'task2'); + }] + }); + }, Error); + test.done(); +}; + +// Issue 988 on github: https://github.com/caolan/async/issues/988 +exports['auto stops running tasks on error'] = function(test) { + async.auto({ + task1: function (callback) { + callback('error'); + }, + task2: function (callback) { + test.ok(false, 'test2 should not be called'); + callback(); + } + }, 1, function (error) { + test.equal(error, 'error', 'finishes with error'); + test.done(); + }); +}; + exports['autoInject'] = function(test){ var callOrder = []; - var testdata = [{test: 'test'}]; async.autoInject({ task1: function(task2, callback){ setTimeout(function(){ @@ -513,8 +642,8 @@ exports['autoInject'] = function(test){ }, task5: function(task2, callback){ setTimeout(function(){ - callOrder.push('task5'); - callback(); + callOrder.push('task5'); + callback(); }, 0); }, task6: function(task2, callback){ @@ -523,6 +652,7 @@ exports['autoInject'] = function(test){ } }, function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.same(callOrder, ['task2','task6','task3','task5','task1','task4']); test.done(); }); @@ -553,6 +683,7 @@ exports['autoInject petrify'] = function (test) { } }, function (err) { + if (err) throw err; test.same(callOrder, ['task2', 'task3', 'task1', 'task4']); test.done(); }); @@ -561,43 +692,44 @@ exports['autoInject petrify'] = function (test) { exports['autoInject results'] = function(test){ var callOrder = []; async.autoInject({ - task1: function(task2, callback){ + task1: function(task2, callback){ test.same(task2, 'task2'); setTimeout(function(){ callOrder.push('task1'); callback(null, 'task1a', 'task1b'); }, 25); }, - task2: function(callback){ + task2: function(callback){ setTimeout(function(){ callOrder.push('task2'); callback(null, 'task2'); }, 50); }, - task3: function(task2, callback){ + task3: function(task2, callback){ test.same(task2, 'task2'); callOrder.push('task3'); callback(null); }, - task4: function(task1, task2, callback){ + task4: function(task1, task2, callback){ test.same(task1, ['task1a','task1b']); test.same(task2, 'task2'); callOrder.push('task4'); callback(null, 'task4'); } }, - function(err, task2, task4, task3, task1){ + function(err, task1, task2, task3, task4){ test.same(callOrder, ['task2','task3','task1','task4']); test.same(task1, ['task1a','task1b']); test.same(task2, 'task2'); - test.same(task3, undefined); - test.same(task4, 'task4'); + test.same(task3, undefined); + test.same(task4, 'task4'); test.done(); }); }; exports['autoInject empty object'] = function(test){ async.autoInject({}, function(err){ + test.ok(err === null, err + " passed instead of 'null'"); test.done(); }); }; @@ -629,6 +761,13 @@ exports['autoInject no callback'] = function(test){ }); }; +exports['autoInject concurrency no callback'] = function(test){ + async.autoInject({ + task1: function(callback){callback();}, + task2: function(task1, callback){callback(); test.done();} + }, 1); +}; + exports['autoInject error should pass partial results'] = function(test) { async.autoInject({ task1: function(callback){ @@ -651,16 +790,16 @@ exports['autoInject error should pass partial results'] = function(test) { // Issue 24 on github: https://github.com/caolan/async/issues#issue/24 // Issue 76 on github: https://github.com/caolan/async/issues#issue/76 -exports['auto removeListener has side effect on loop iterator'] = function(test) { - async.auto({ - task1: ['task3', function(/*callback*/) { test.done(); }], - task2: ['task3', function(/*callback*/) { /* by design: DON'T call callback */ }], +exports['autoInject removeListener has side effect on loop iterator'] = function(test) { + async.autoInject({ + task1: function(task3, callback) { test.done(); }, + task2: function(task3, callback) { /* by design: DON'T call callback */ }, task3: function(callback) { callback(); } }); }; // Issue 410 on github: https://github.com/caolan/async/issues/410 -exports['auto calls callback multiple times'] = function(test) { +exports['autoInject calls callback multiple times'] = function(test) { if (isBrowser()) { // node only test test.done(); @@ -675,9 +814,9 @@ exports['auto calls callback multiple times'] = function(test) { } }); domain.run(function () { - async.auto({ + async.autoInject({ task1: function(callback) { callback(null); }, - task2: ['task1', function(callback) { callback(null); }] + task2: function(task1, callback) { callback(null); } }, // Error throwing final callback. This should only run once @@ -697,9 +836,9 @@ exports['auto calls callback multiple times'] = function(test) { }; -exports['auto calls callback multiple times with parallel functions'] = function(test) { +exports['autoInject calls callback multiple times with parallel functions'] = function(test) { test.expect(1); - async.auto({ + async.autoInject({ task1: function(callback) { setTimeout(callback,0,"err"); }, task2: function(callback) { setTimeout(callback,0,"err"); } }, @@ -711,74 +850,33 @@ exports['auto calls callback multiple times with parallel functions'] = function }; -// Issue 462 on github: https://github.com/caolan/async/issues/462 -exports['auto modifying results causes final callback to run early'] = function(test) { - async.auto({ - task1: function(callback, results){ - results.inserted = true; - callback(null, 'task1'); - }, - task2: function(callback){ - setTimeout(function(){ - callback(null, 'task2'); - }, 50); - }, - task3: function(callback){ - setTimeout(function(){ - callback(null, 'task3'); - }, 100); - } - }, - function(err, results){ - test.equal(results.inserted, true); - test.ok(results.task3, 'task3'); - test.done(); - }); -}; - // Issue 263 on github: https://github.com/caolan/async/issues/263 -exports['auto prevent dead-locks due to inexistant dependencies'] = function(test) { +exports['autoInject prevent dead-locks due to inexistant dependencies'] = function(test) { test.throws(function () { - async.auto({ - task1: ['noexist', function(callback){ + async.autoInject({ + task1: function(noexist, callback){ callback(null, 'task1'); - }] + } }); }, Error); test.done(); }; // Issue 263 on github: https://github.com/caolan/async/issues/263 -exports['auto prevent dead-locks due to cyclic dependencies'] = function(test) { +exports['autoInject prevent dead-locks due to cyclic dependencies'] = function(test) { test.throws(function () { - async.auto({ - task1: ['task2', function(callback){ + async.autoInject({ + task1: function(task2, callback){ callback(null, 'task1'); - }], - task2: ['task1', function(callback){ + }, + task2: function(task1, callback){ callback(null, 'task2'); - }] + } }); }, Error); test.done(); }; -// Issue 988 on github: https://github.com/caolan/async/issues/988 -exports['auto stops running tasks on error'] = function(test) { - async.auto({ - task1: function (callback) { - callback('error'); - }, - task2: function (callback) { - test.ok(false, 'test2 should not be called'); - callback(); - } - }, 1, function (error) { - test.equal(error, 'error', 'finishes with error'); - test.done(); - }); -}; - // Issue 306 on github: https://github.com/caolan/async/issues/306 exports['retry when attempt succeeds'] = function(test) { var failed = 3; From 0858e09f203f9e358bdfa1ce84400e758bea68fc Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Sun, 6 Mar 2016 17:18:16 +0000 Subject: [PATCH 4/8] Support concurrency parameter. --- lib/async.js | 8 +++++++- test/test-async.js | 48 ++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+), 1 deletion(-) diff --git a/lib/async.js b/lib/async.js index dbdb090c5..f3450ce80 100755 --- a/lib/async.js +++ b/lib/async.js @@ -619,7 +619,12 @@ return result; }; - async.autoInject = function(tasks, callback) { + async.autoInject = function(tasks, concurrency, callback) { + if (typeof arguments[1] === 'function') { + // concurrency is optional, shift the args. + callback = concurrency; + concurrency = null; + } var injTasks = {}; for (var i in tasks) { injTasks[i] = (function(val) { @@ -644,6 +649,7 @@ return this.auto( injTasks, + concurrency, callback && function(err, results) { callback.apply(null, [err].concat(_map(_funcParamNames(callback).slice(1), function(a) { return results[a]; }))); } diff --git a/test/test-async.js b/test/test-async.js index 325333624..79178adca 100755 --- a/test/test-async.js +++ b/test/test-async.js @@ -658,6 +658,38 @@ exports['autoInject'] = function(test){ }); }; +exports['autoInject concurrency'] = function (test) { + var concurrency = 2; + var runningTasks = []; + var makeCallback = function(taskName) { + return function(callback) { + runningTasks.push(taskName); + setTimeout(function(){ + // Each task returns the array of running tasks as results. + var result = runningTasks.slice(0); + runningTasks.splice(runningTasks.indexOf(taskName), 1); + callback(null, result); + }); + }; + }; + async.autoInject({ + task1: function(task2, callback) { makeCallback('task1')(callback); }, + task2: function(callback) { makeCallback('task2')(callback); }, + task3: function(task2, callback) { makeCallback('task3')(callback); }, + task4: function(task1, task2, callback) { makeCallback('task4')(callback); }, + task5: function(task2, callback) { makeCallback('task5')(callback); }, + task6: function(task2, callback) { makeCallback('task6')(callback); } + }, concurrency, function(err, task1, task2, task3, task4, task5, task6){ + test.ok(task1.length <= concurrency); + test.ok(task2.length <= concurrency); + test.ok(task3.length <= concurrency); + test.ok(task4.length <= concurrency); + test.ok(task5.length <= concurrency); + test.ok(task6.length <= concurrency); + test.done(); + }); +}; + exports['autoInject petrify'] = function (test) { var callOrder = []; async.autoInject({ @@ -877,6 +909,22 @@ exports['autoInject prevent dead-locks due to cyclic dependencies'] = function(t test.done(); }; +// Issue 988 on github: https://github.com/caolan/async/issues/988 +exports['autoInject stops running tasks on error'] = function(test) { + async.autoInject({ + task1: function (callback) { + callback('error'); + }, + task2: function (callback) { + test.ok(false, 'test2 should not be called'); + callback(); + } + }, 1, function (error) { + test.equal(error, 'error', 'finishes with error'); + test.done(); + }); +}; + // Issue 306 on github: https://github.com/caolan/async/issues/306 exports['retry when attempt succeeds'] = function(test) { var failed = 3; From b42f072a3b98ca224dda68ca0ac0972d7db33274 Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Wed, 6 Apr 2016 01:07:57 +0100 Subject: [PATCH 5/8] Duplicate section removed and typo fixed. --- README.md | 69 +------------------------------------------------------ 1 file changed, 1 insertion(+), 68 deletions(-) diff --git a/README.md b/README.md index 6a8ef52d0..77de89f9c 100644 --- a/README.md +++ b/README.md @@ -1498,7 +1498,7 @@ __Arguments__ * `tasks` - An object, each of whose properties is a function of the form 'func([dependencies...], callback). The object's key of a property serves as the name of the task defined by that property, i.e. can be used when specifying requirements for other tasks. * The `callback` parameter is a `callback(err, result)` which must be called when finished, passing an `error` (which can be `null`) and the result of the function's execution. The remaining parameters name other tasks on which the task is dependent, and the results from those tasks are the arguments of those parameters. -* `callback(err, [results...])` - An optional callback which is called when all the tasks have been completed. It receives the `err` argument if any `tasks` pass an error to their callback. The remaining parameters are task names whose results you are interested in. This callback will only be called when all tasks have finished or an error has occurred, and so do not not specify dependencies in the same way as `tasks` do. If an error occurs, no further `tasks` will be performed, and `results` will only be valid for those tasks which managed to complete. +* `callback(err, [results...])` - An optional callback which is called when all the tasks have been completed. It receives the `err` argument if any `tasks` pass an error to their callback. The remaining parameters are task names whose results you are interested in. This callback will only be called when all tasks have finished or an error has occurred, and so do not specify dependencies in the same way as `tasks` do. If an error occurs, no further `tasks` will be performed, and `results` will only be valid for those tasks which managed to complete. __Example__ @@ -1550,73 +1550,6 @@ async.autoInject({ This still has an advantage over plain `auto`, since the results a task depends on are still spread into arguments. ---------------------------------------- - - -### autoInject(tasks, [callback]) - -A dependency-injected version of the [`auto`](#auto) function. Dependent tasks are -specified as parameters to the function, after the usual callback parameter, with the -parameter names matching the names of the tasks it depends on. This can provide even more -readable task graphs which can be easier to maintain. - -If a final callback is specified, the task results are similarly injected, specified as -named parameters after the initial error parameter. - -The autoInject function is purely syntactic sugar and its semantics are otherwise -equivalent to [`auto`](#auto). - -__Arguments__ - -* `tasks` - An object, each of whose properties is a function of the form - 'func(callback, [dependencies...]). The object's key of a property serves as the - name of the task defined by that property, i.e. can be used when specifying requirements - for other tasks. - The `callback` parameter is a `callback(err, result)` which must be called when finished, - passing an `error` (which can be `null`) and the result of the function's execution. - The remaining parameters name other tasks on which the task is dependent, and the results - from those tasks are the arguments of those parameters. -* `callback(err, [results...])` - An optional callback which is called when all the - tasks have been completed. It receives the `err` argument if any `tasks` - pass an error to their callback. The remaining parameters are task names whose results - you are interested in. This callback will only be called when all tasks have finished or - an error has occurred, and so do not not specify dependencies in the same way as `tasks` - do. If an error occurs, no further `tasks` will be performed, and `results` will only be - valid for those tasks which managed to complete. - - -__Example__ - -The example from [`auto`](#auto) can be rewritten as follows: - -```js -async.autoInject({ - get_data: function(callback){ - // async code to get some data - callback(null, 'data', 'converted to array'); - }, - make_folder: function(callback){ - // async code to create a directory to store a file in - // this is run at the same time as getting the data - callback(null, 'folder'); - }, - write_file: function(callback, get_data, make_folder){ - // once there is some data and the directory exists, - // write the data to a file in the directory - callback(null, 'filename'); - }, - email_link: function(callback, write_file){ - // once the file is written let's email a link to it... - // write_file contains the filename returned by write_file. - callback(null, {'file':write_file, 'email':'user@example.com'}); - } -}, function(err, email_link) { - console.log('err = ', err); - console.log('email_link = ', email_link); -}); -``` - - --------------------------------------- From 33556e09a186e08557dc03b291d55ce5ee909ba2 Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Wed, 6 Apr 2016 02:35:26 +0100 Subject: [PATCH 6/8] Fix for injection into final callback. --- lib/autoInject.js | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/autoInject.js b/lib/autoInject.js index 92594878c..245db8e81 100644 --- a/lib/autoInject.js +++ b/lib/autoInject.js @@ -42,5 +42,21 @@ export default function autoInject(tasks, callback) { } }); - auto(newTasks, callback); + auto(newTasks, function (err, results) { + var params; + if (isArray(callback)) { + params = clone(callback); + callback = params.pop(); + } else { + params = parseParams(callback); + params.shift(); + } + + params = arrayMap(params, function (name) { + return results[name]; + }); + + params.unshift(err); + callback.apply(null, params); + }); } From b5bba26894d454183070c7e9f5cf456b05a6f1b3 Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Wed, 6 Apr 2016 02:40:15 +0100 Subject: [PATCH 7/8] Array results test. --- mocha_test/autoInject.js | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/mocha_test/autoInject.js b/mocha_test/autoInject.js index 059c7ae0d..1408af981 100644 --- a/mocha_test/autoInject.js +++ b/mocha_test/autoInject.js @@ -44,8 +44,8 @@ describe('autoInject', function () { callback(null, 6); } }, - function(err, results){ - expect(results.task6).to.equal(6); + function(err, task6){ + expect(task6).to.equal(6); expect(callOrder).to.eql(['task2','task3','task6','task5','task1','task4']); done(); }); @@ -74,4 +74,22 @@ describe('autoInject', function () { }); }); + it('should work with array results', function (done) { + async.autoInject({ + task1: function (cb) { + cb(null, 1); + }, + task2: function (task3, cb) { + cb(null, 2); + }, + task3: function (cb) { + cb(null, 3); + } + }, ['task3', 'task1', function (err, task3, task1) { + expect(task1).to.equal(1); + expect(task3).to.equal(3); + done(); + }]); + }); + }); From 3e33843000cba809a52364f10cbb48b385025b46 Mon Sep 17 00:00:00 2001 From: Steve Robb Date: Wed, 6 Apr 2016 02:45:13 +0100 Subject: [PATCH 8/8] Docs updated to include injected results into final callback. --- README.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 77de89f9c..93c7a2f04 100644 --- a/README.md +++ b/README.md @@ -1532,7 +1532,7 @@ async.autoInject({ }); ``` -If you are using a JS minifier that mangles parameter names, `autoInject` will not work with plain functions, since the parameter names will be collapsed to a single letter identifier. To work around this, you can explicitly specify the names of the parameters your task function needs in an array, similar to Angular.js dependency injection. +If you are using a JS minifier that mangles parameter names, `autoInject` will not work with plain functions, since the parameter names will be collapsed to a single letter identifier. To work around this, you can explicitly specify the names of the parameters your task function needs in an array, similar to Angular.js dependency injection. The final results callback can be provided as an array in the same way. ```js async.autoInject({ @@ -1544,7 +1544,10 @@ async.autoInject({ callback(null, {'file':write_file, 'email':'user@example.com'}); }] //... -}, done); +}, ['email_link', function(err, email_link) { + console.log('err = ', err); + console.log('email_link = ', email_link); +}]); ``` This still has an advantage over plain `auto`, since the results a task depends on are still spread into arguments.