From 27230c22ccc567f7944b0dc7be23148c5e7304b0 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Tue, 8 Mar 2016 16:17:25 -0800 Subject: [PATCH 1/2] added autoInject, adapted from #608 --- README.md | 74 +++++++++++++++++++++++++++++++++++++- lib/autoInject.js | 45 +++++++++++++++++++++++ lib/index.js | 3 ++ mocha_test/autoInject.js | 77 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 198 insertions(+), 1 deletion(-) create mode 100644 lib/autoInject.js create mode 100644 mocha_test/autoInject.js diff --git a/README.md b/README.md index d527b4286..fab0a7437 100644 --- a/README.md +++ b/README.md @@ -219,6 +219,7 @@ Some functions are also available in the following forms: * [`queue`](#queue), [`priorityQueue`](#priorityQueue) * [`cargo`](#cargo) * [`auto`](#auto) +* [`autoInject`](#autoInject) * [`retry`](#retry) * [`iterator`](#iterator) * [`times`](#times), `timesSeries`, `timesLimit` @@ -1291,7 +1292,7 @@ methods: * `saturated` - A callback that is called when the `queue.length()` hits the concurrency and further tasks will be queued. * `empty` - A callback that is called when the last item from the `queue` is given to a `worker`. * `drain` - A callback that is called when the last item from the `queue` has returned from the `worker`. -* `idle()`, `pause()`, `resume()`, `kill()` - cargo inherits all of the same methods and event calbacks as [`queue`](#queue) +* `idle()`, `pause()`, `resume()`, `kill()` - cargo inherits all of the same methods and event callbacks as [`queue`](#queue) __Example__ @@ -1420,6 +1421,77 @@ function(err, results){ For a complicated series of `async` tasks, using the [`auto`](#auto) function makes adding 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([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. + + +__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(get_data, make_folder, callback){ + // once there is some data and the directory exists, + // write the data to a file in the directory + callback(null, 'filename'); + }, + email_link: function(write_file, callback){ + // 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); +}); +``` + +If you are using a minifier that mangles parameter names, `autoInject` will not work with plain functions. To work around this, you can explicitly specify the names of the parameters in an array, similar to Angular.js dependency injection. + +```js +async.autoInject({ + //... + write_file: ['get_data', 'make_folder', function(get_data, make_folder, callback){ + // once there is some data and the directory exists, + // write the data to a file in the directory + callback(null, 'filename'); + }], + email_link: ['write_file', function(write_file, callback){ + // 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'}); + }] + //... +}, +``` + +This still has an advantage over plain `auto`, since the results a task depends on are still spread into arguments. + + --------------------------------------- diff --git a/lib/autoInject.js b/lib/autoInject.js new file mode 100644 index 000000000..1b6fea43a --- /dev/null +++ b/lib/autoInject.js @@ -0,0 +1,45 @@ +import auto from './auto'; +import forOwn from 'lodash/forOwn'; +import arrayMap from 'lodash/_arrayMap'; +import isArray from 'lodash/isArray'; + +var argsRegex = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; + +function parseParams(func) { + return func.toString().match(argsRegex)[1].split(/\s*\,\s*/); +} + +export default function autoInject(tasks, callback) { + var newTasks = {}; + + forOwn(tasks, function (taskFn, key) { + var params; + + if (isArray(taskFn)) { + params = [...taskFn]; + taskFn = params.pop(); + + newTasks[key] = [...params].concat(newTask); + } else if (taskFn.length === 0) { + throw new Error("autoInject task functions require explicit parameters."); + } else if (taskFn.length === 1) { + // no dependencies + newTasks[key] = taskFn; + } else { + params = parseParams(taskFn); + params.pop(); + + newTasks[key] = [...params].concat(newTask); + + } + + function newTask(results, taskCb) { + var newArgs = arrayMap(params, function (name) { + return results[name]; + }); + taskFn(...newArgs.concat(taskCb)); + } + }); + + auto(newTasks, callback); +} diff --git a/lib/index.js b/lib/index.js index 300491dda..3db406497 100644 --- a/lib/index.js +++ b/lib/index.js @@ -5,6 +5,7 @@ import applyEachSeries from './applyEachSeries'; import apply from './apply'; import asyncify from './asyncify'; import auto from './auto'; +import autoInject from './autoInject'; import cargo from './cargo'; import compose from './compose'; import concat from './concat'; @@ -71,6 +72,7 @@ export default { apply: apply, asyncify: asyncify, auto: auto, + autoInject: autoInject, cargo: cargo, compose: compose, concat: concat, @@ -155,6 +157,7 @@ export { apply as apply, asyncify as asyncify, auto as auto, + autoInject as autoInject, cargo as cargo, compose as compose, concat as concat, diff --git a/mocha_test/autoInject.js b/mocha_test/autoInject.js new file mode 100644 index 000000000..53a3d1755 --- /dev/null +++ b/mocha_test/autoInject.js @@ -0,0 +1,77 @@ +var async = require('../lib'); +var expect = require('chai').expect; +var _ = require('lodash'); + +describe('autoInject', function () { + + it("basics", function (done) { + var callOrder = []; + async.autoInject({ + task1: function(task2, callback){ + expect(task2).to.equal(2); + setTimeout(function(){ + callOrder.push('task1'); + callback(null, 1); + }, 25); + }, + task2: function(callback){ + setTimeout(function(){ + callOrder.push('task2'); + callback(null, 2); + }, 50); + }, + task3: function(task2, callback){ + expect(task2).to.equal(2); + callOrder.push('task3'); + callback(null, 3); + }, + task4: function(task1, task2, callback){ + expect(task1).to.equal(1); + expect(task2).to.equal(2); + callOrder.push('task4'); + callback(null, 4); + }, + task5: function(task2, callback){ + expect(task2).to.equal(2); + setTimeout(function(){ + callOrder.push('task5'); + callback(null, 5); + }, 0); + }, + task6: function(task2, callback){ + expect(task2).to.equal(2); + callOrder.push('task6'); + callback(null, 6); + } + }, + function(err, results){ + expect(results.task6).to.equal(6); + expect(callOrder).to.eql(['task2','task6','task3','task5','task1','task4']); + done(); + }); + }); + + it('should work with array tasks', function (done) { + var callOrder = []; + + async.autoInject({ + task1: function (cb) { + callOrder.push('task1'); + cb(null, 1); + }, + task2: ['task3', function (task3, cb) { + expect(task3).to.equal(3); + callOrder.push('task2'); + cb(null, 2); + }], + task3: function (cb) { + callOrder.push('task3'); + cb(null, 3); + } + }, function () { + expect(callOrder).to.eql(['task1','task3','task2']); + done(); + }); + }); + +}); From df31042d689ba51e383209b2836ad971c11dee07 Mon Sep 17 00:00:00 2001 From: Alexander Early Date: Wed, 9 Mar 2016 13:06:28 -0800 Subject: [PATCH 2/2] remove ES6 idioms --- README.md | 6 +----- lib/autoInject.js | 13 +++++++------ 2 files changed, 8 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index fab0a7437..b872bc8d3 100644 --- a/README.md +++ b/README.md @@ -1470,19 +1470,15 @@ async.autoInject({ }); ``` -If you are using a minifier that mangles parameter names, `autoInject` will not work with plain functions. To work around this, you can explicitly specify the names of the parameters 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. ```js async.autoInject({ //... write_file: ['get_data', 'make_folder', function(get_data, make_folder, callback){ - // once there is some data and the directory exists, - // write the data to a file in the directory callback(null, 'filename'); }], email_link: ['write_file', function(write_file, callback){ - // 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'}); }] //... diff --git a/lib/autoInject.js b/lib/autoInject.js index 1b6fea43a..f84a4d999 100644 --- a/lib/autoInject.js +++ b/lib/autoInject.js @@ -1,6 +1,7 @@ import auto from './auto'; import forOwn from 'lodash/forOwn'; import arrayMap from 'lodash/_arrayMap'; +import clone from 'lodash/_baseClone'; import isArray from 'lodash/isArray'; var argsRegex = /^function\s*[^\(]*\(\s*([^\)]*)\)/m; @@ -16,28 +17,28 @@ export default function autoInject(tasks, callback) { var params; if (isArray(taskFn)) { - params = [...taskFn]; + params = clone(taskFn); taskFn = params.pop(); - newTasks[key] = [...params].concat(newTask); + newTasks[key] = clone(params).concat(newTask); } else if (taskFn.length === 0) { throw new Error("autoInject task functions require explicit parameters."); } else if (taskFn.length === 1) { - // no dependencies + // no dependencies, use the function as-is newTasks[key] = taskFn; } else { params = parseParams(taskFn); params.pop(); - newTasks[key] = [...params].concat(newTask); - + newTasks[key] = clone(params).concat(newTask); } function newTask(results, taskCb) { var newArgs = arrayMap(params, function (name) { return results[name]; }); - taskFn(...newArgs.concat(taskCb)); + newArgs.push(taskCb); + taskFn.apply(null, newArgs); } });