UNPKG

11.9 kBJavaScriptView Raw
1'use strict';
2
3Object.defineProperty(exports, "__esModule", {
4 value: true
5});
6exports.default = auto;
7
8var _once = require('./internal/once.js');
9
10var _once2 = _interopRequireDefault(_once);
11
12var _onlyOnce = require('./internal/onlyOnce.js');
13
14var _onlyOnce2 = _interopRequireDefault(_onlyOnce);
15
16var _wrapAsync = require('./internal/wrapAsync.js');
17
18var _wrapAsync2 = _interopRequireDefault(_wrapAsync);
19
20var _promiseCallback = require('./internal/promiseCallback.js');
21
22function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }
23
24/**
25 * Determines the best order for running the {@link AsyncFunction}s in `tasks`, based on
26 * their requirements. Each function can optionally depend on other functions
27 * being completed first, and each function is run as soon as its requirements
28 * are satisfied.
29 *
30 * If any of the {@link AsyncFunction}s pass an error to their callback, the `auto` sequence
31 * will stop. Further tasks will not execute (so any other functions depending
32 * on it will not run), and the main `callback` is immediately called with the
33 * error.
34 *
35 * {@link AsyncFunction}s also receive an object containing the results of functions which
36 * have completed so far as the first argument, if they have dependencies. If a
37 * task function has no dependencies, it will only be passed a callback.
38 *
39 * @name auto
40 * @static
41 * @memberOf module:ControlFlow
42 * @method
43 * @category Control Flow
44 * @param {Object} tasks - An object. Each of its properties is either a
45 * function or an array of requirements, with the {@link AsyncFunction} itself the last item
46 * in the array. The object's key of a property serves as the name of the task
47 * defined by that property, i.e. can be used when specifying requirements for
48 * other tasks. The function receives one or two arguments:
49 * * a `results` object, containing the results of the previously executed
50 * functions, only passed if the task has any dependencies,
51 * * a `callback(err, result)` function, which must be called when finished,
52 * passing an `error` (which can be `null`) and the result of the function's
53 * execution.
54 * @param {number} [concurrency=Infinity] - An optional `integer` for
55 * determining the maximum number of tasks that can be run in parallel. By
56 * default, as many as possible.
57 * @param {Function} [callback] - An optional callback which is called when all
58 * the tasks have been completed. It receives the `err` argument if any `tasks`
59 * pass an error to their callback. Results are always returned; however, if an
60 * error occurs, no further `tasks` will be performed, and the results object
61 * will only contain partial results. Invoked with (err, results).
62 * @returns {Promise} a promise, if a callback is not passed
63 * @example
64 *
65 * //Using Callbacks
66 * async.auto({
67 * get_data: function(callback) {
68 * // async code to get some data
69 * callback(null, 'data', 'converted to array');
70 * },
71 * make_folder: function(callback) {
72 * // async code to create a directory to store a file in
73 * // this is run at the same time as getting the data
74 * callback(null, 'folder');
75 * },
76 * write_file: ['get_data', 'make_folder', function(results, callback) {
77 * // once there is some data and the directory exists,
78 * // write the data to a file in the directory
79 * callback(null, 'filename');
80 * }],
81 * email_link: ['write_file', function(results, callback) {
82 * // once the file is written let's email a link to it...
83 * callback(null, {'file':results.write_file, 'email':'user@example.com'});
84 * }]
85 * }, function(err, results) {
86 * if (err) {
87 * console.log('err = ', err);
88 * }
89 * console.log('results = ', results);
90 * // results = {
91 * // get_data: ['data', 'converted to array']
92 * // make_folder; 'folder',
93 * // write_file: 'filename'
94 * // email_link: { file: 'filename', email: 'user@example.com' }
95 * // }
96 * });
97 *
98 * //Using Promises
99 * async.auto({
100 * get_data: function(callback) {
101 * console.log('in get_data');
102 * // async code to get some data
103 * callback(null, 'data', 'converted to array');
104 * },
105 * make_folder: function(callback) {
106 * console.log('in make_folder');
107 * // async code to create a directory to store a file in
108 * // this is run at the same time as getting the data
109 * callback(null, 'folder');
110 * },
111 * write_file: ['get_data', 'make_folder', function(results, callback) {
112 * // once there is some data and the directory exists,
113 * // write the data to a file in the directory
114 * callback(null, 'filename');
115 * }],
116 * email_link: ['write_file', function(results, callback) {
117 * // once the file is written let's email a link to it...
118 * callback(null, {'file':results.write_file, 'email':'user@example.com'});
119 * }]
120 * }).then(results => {
121 * console.log('results = ', results);
122 * // results = {
123 * // get_data: ['data', 'converted to array']
124 * // make_folder; 'folder',
125 * // write_file: 'filename'
126 * // email_link: { file: 'filename', email: 'user@example.com' }
127 * // }
128 * }).catch(err => {
129 * console.log('err = ', err);
130 * });
131 *
132 * //Using async/await
133 * async () => {
134 * try {
135 * let results = await async.auto({
136 * get_data: function(callback) {
137 * // async code to get some data
138 * callback(null, 'data', 'converted to array');
139 * },
140 * make_folder: function(callback) {
141 * // async code to create a directory to store a file in
142 * // this is run at the same time as getting the data
143 * callback(null, 'folder');
144 * },
145 * write_file: ['get_data', 'make_folder', function(results, callback) {
146 * // once there is some data and the directory exists,
147 * // write the data to a file in the directory
148 * callback(null, 'filename');
149 * }],
150 * email_link: ['write_file', function(results, callback) {
151 * // once the file is written let's email a link to it...
152 * callback(null, {'file':results.write_file, 'email':'user@example.com'});
153 * }]
154 * });
155 * console.log('results = ', results);
156 * // results = {
157 * // get_data: ['data', 'converted to array']
158 * // make_folder; 'folder',
159 * // write_file: 'filename'
160 * // email_link: { file: 'filename', email: 'user@example.com' }
161 * // }
162 * }
163 * catch (err) {
164 * console.log(err);
165 * }
166 * }
167 *
168 */
169function auto(tasks, concurrency, callback) {
170 if (typeof concurrency !== 'number') {
171 // concurrency is optional, shift the args.
172 callback = concurrency;
173 concurrency = null;
174 }
175 callback = (0, _once2.default)(callback || (0, _promiseCallback.promiseCallback)());
176 var numTasks = Object.keys(tasks).length;
177 if (!numTasks) {
178 return callback(null);
179 }
180 if (!concurrency) {
181 concurrency = numTasks;
182 }
183
184 var results = {};
185 var runningTasks = 0;
186 var canceled = false;
187 var hasError = false;
188
189 var listeners = Object.create(null);
190
191 var readyTasks = [];
192
193 // for cycle detection:
194 var readyToCheck = []; // tasks that have been identified as reachable
195 // without the possibility of returning to an ancestor task
196 var uncheckedDependencies = {};
197
198 Object.keys(tasks).forEach(key => {
199 var task = tasks[key];
200 if (!Array.isArray(task)) {
201 // no dependencies
202 enqueueTask(key, [task]);
203 readyToCheck.push(key);
204 return;
205 }
206
207 var dependencies = task.slice(0, task.length - 1);
208 var remainingDependencies = dependencies.length;
209 if (remainingDependencies === 0) {
210 enqueueTask(key, task);
211 readyToCheck.push(key);
212 return;
213 }
214 uncheckedDependencies[key] = remainingDependencies;
215
216 dependencies.forEach(dependencyName => {
217 if (!tasks[dependencyName]) {
218 throw new Error('async.auto task `' + key + '` has a non-existent dependency `' + dependencyName + '` in ' + dependencies.join(', '));
219 }
220 addListener(dependencyName, () => {
221 remainingDependencies--;
222 if (remainingDependencies === 0) {
223 enqueueTask(key, task);
224 }
225 });
226 });
227 });
228
229 checkForDeadlocks();
230 processQueue();
231
232 function enqueueTask(key, task) {
233 readyTasks.push(() => runTask(key, task));
234 }
235
236 function processQueue() {
237 if (canceled) return;
238 if (readyTasks.length === 0 && runningTasks === 0) {
239 return callback(null, results);
240 }
241 while (readyTasks.length && runningTasks < concurrency) {
242 var run = readyTasks.shift();
243 run();
244 }
245 }
246
247 function addListener(taskName, fn) {
248 var taskListeners = listeners[taskName];
249 if (!taskListeners) {
250 taskListeners = listeners[taskName] = [];
251 }
252
253 taskListeners.push(fn);
254 }
255
256 function taskComplete(taskName) {
257 var taskListeners = listeners[taskName] || [];
258 taskListeners.forEach(fn => fn());
259 processQueue();
260 }
261
262 function runTask(key, task) {
263 if (hasError) return;
264
265 var taskCallback = (0, _onlyOnce2.default)((err, ...result) => {
266 runningTasks--;
267 if (err === false) {
268 canceled = true;
269 return;
270 }
271 if (result.length < 2) {
272 [result] = result;
273 }
274 if (err) {
275 var safeResults = {};
276 Object.keys(results).forEach(rkey => {
277 safeResults[rkey] = results[rkey];
278 });
279 safeResults[key] = result;
280 hasError = true;
281 listeners = Object.create(null);
282 if (canceled) return;
283 callback(err, safeResults);
284 } else {
285 results[key] = result;
286 taskComplete(key);
287 }
288 });
289
290 runningTasks++;
291 var taskFn = (0, _wrapAsync2.default)(task[task.length - 1]);
292 if (task.length > 1) {
293 taskFn(results, taskCallback);
294 } else {
295 taskFn(taskCallback);
296 }
297 }
298
299 function checkForDeadlocks() {
300 // Kahn's algorithm
301 // https://en.wikipedia.org/wiki/Topological_sorting#Kahn.27s_algorithm
302 // http://connalle.blogspot.com/2013/10/topological-sortingkahn-algorithm.html
303 var currentTask;
304 var counter = 0;
305 while (readyToCheck.length) {
306 currentTask = readyToCheck.pop();
307 counter++;
308 getDependents(currentTask).forEach(dependent => {
309 if (--uncheckedDependencies[dependent] === 0) {
310 readyToCheck.push(dependent);
311 }
312 });
313 }
314
315 if (counter !== numTasks) {
316 throw new Error('async.auto cannot execute tasks due to a recursive dependency');
317 }
318 }
319
320 function getDependents(taskName) {
321 var result = [];
322 Object.keys(tasks).forEach(key => {
323 const task = tasks[key];
324 if (Array.isArray(task) && task.indexOf(taskName) >= 0) {
325 result.push(key);
326 }
327 });
328 return result;
329 }
330
331 return callback[_promiseCallback.PROMISE_SYMBOL];
332}
333module.exports = exports.default;
\No newline at end of file