index.js
function _execute(err) {
	var self = this;
	if (err) return this._finalize(err); // An error has been raised - stop exec and call finalize now

	if (!self._executing) { // Never before run this object - run fire('start') and defer until it finishes
		self._executing = true;
		return self.fire.call(self, 'start', self._execute.bind(self));
	}

	do {
		var redo = false;
		if (self._structPointer >= self._struct.length) return this._finalize(err); // Nothing more to execute in struct
		self._deferCheck(); // Kick off any pending deferred items
		var currentExec = self._struct[self._structPointer];
		// Sanity checks {{{
		if (!currentExec.type) {
			throw new Error('No type is specified for async-chainable structure at offset ' + self._structPointer);
			return self;
		}
		// }}}
		self._structPointer++;

		// Skip step when function supports skipping if the argument is empty {{{
		if (
			[
				'parallelArray', 'parallelObject',
				'forEachArray', 'forEachObject',
				'seriesArray', 'seriesObject',
				'deferArray', 'deferObject',
				'set'
			].indexOf(currentExec.type) > -1 &&
			(
				!currentExec.payload || // Not set OR
				(argy.isType(currentExec.payload, 'array') && !currentExec.payload.length) || // An empty array
				(argy.isType(currentExec.payload, 'object') && !Object.keys(currentExec.payload).length) // An empty object
			)
		) {
			currentExec.completed = true;
			redo = true;
			continue;
		}
		// }}}

		switch (currentExec.type) {
			case 'forEachRange':
				var iterArray = Array(currentExec.max - currentExec.min + 1).fill(false);
				self.runArray(iterArray.map(function(v, i) {
					var val = self._context._item = currentExec.min + i;
					var index = self._context._key = i;
					return function(next) {
						if (currentExec.translate) val = currentExec.translate(val, index, iterArray.length);
						run(self._options.context, currentExec.callback, next, [val, index, iterArray.length]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachArray':
				self.runArray(currentExec.payload.map(function(item, iter) {
					self._context._item = item;
					self._context._key = iter;
					return function(next) {
						run(self._options.context, currentExec.callback, next, [item, iter]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachObject':
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						self._context._item = currentExec.payload[key];
						self._context._key = key;
						run(self._options.context, currentExec.callback, function(err, value) {
							self._set(key, value); // Allocate returned value to context
							next(err);
						}, [currentExec.payload[key], key]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachLateBound':
				if (!currentExec.payload || !currentExec.payload.length) { // Payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				var resolvedPayload = self.getPath(self._context, currentExec.payload);
				if (!resolvedPayload) { // Resolved payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				// Replace own exec array with actual type of payload now we know what it is {{{
				if (argy.isType(resolvedPayload, 'array')) {
					currentExec.type = 'forEachArray';
				} else if (argy.isType(resolvedPayload, 'object')) {
					currentExec.type = 'forEachObject';
				} else {
					throw new Error('Cannot perform forEach over unknown object type: ' + argy.getType(resolvedPayload));
				}
				currentExec.payload = resolvedPayload;
				self._structPointer--; // Force re-eval of this chain item now its been replace with its real (late-bound) type
				redo = true;
				// }}}
				break;

			case 'mapArray':
				var output = new Array(currentExec.payload.length);
				self.runArray(currentExec.payload.map(function(item, iter) {
					self._context._item = item;
					self._context._key = iter;
					return function(next) {
						run(self._options.context, currentExec.callback, function(err, value) {
							if (err) return next(err);
							output[iter] = value;
							next();
						}, [item, iter]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._set(currentExec.output, output);
					self._execute(err);
				});
				break;
			case 'mapObject':
				var output = {};
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						self._context._item = currentExec.payload[key];
						self._context._key = key;
						run(self._options.context, currentExec.callback, function(err, value, outputKey) {
							output[outputKey || key] = value;
							next(err);
						}, [currentExec.payload[key], key]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._set(currentExec.output, output);
					self._execute(err);
				});
				break;
			case 'mapLateBound':
				if (!currentExec.payload || !currentExec.payload.length) { // Payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				var resolvedPayload = self.getPath(self._context, currentExec.payload);
				if (!resolvedPayload) { // Resolved payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				// Replace own exec array with actual type of payload now we know what it is {{{
				if (argy.isType(resolvedPayload, 'array')) {
					currentExec.type = 'mapArray';
				} else if (argy.isType(resolvedPayload, 'object')) {
					currentExec.type = 'mapObject';
				} else {
					throw new Error('Cannot perform map over unknown object type: ' + argy.getType(resolvedPayload));
				}
				currentExec.payload = resolvedPayload;
				self._structPointer--; // Force re-eval of this chain item now its been replace with its real (late-bound) type
				redo = true;
				// }}}
				break;
			case 'parallelArray':
			case 'seriesArray':
				self.runArray(currentExec.payload.map(function(task) {
					return function(next) {
						run(self._options.context, task, next);
					};
				}), currentExec.type == 'parallelArray' ? self._options.limit : 1, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'seriesObject':
			case 'parallelObject':
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						run(self._options.context, currentExec.payload[key], function(err, value) {
							self._set(key, value); // Allocate returned value to context
							next(err);
						});
					};
				}), currentExec.type == 'parallelObject' ? self._options.limit : 1, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'race':
				var hasResult = false;
				var hasError = false;
				self.runArray(currentExec.payload.map(function(task) {
					return function(next) {
						run(self._options.context, task, function(err, taskResult) {
							if (err) {
								hasError = true
								next(err, taskResult);
							} else if (!hasResult && !hasError && taskResult !== null && typeof taskResult !== 'undefined') {
								self._set(currentExec.id, taskResult); // Allocate returned value to context
								hasResult = true;
								next('!RACEDONE!', taskResult); // Force an error to stop the run() function
							} else {
								next(err, taskResult);
							}
						});
					};
				}), self._options.limit, function(err, val) {
					currentExec.completed = true;

					// Override race finish error as it was just to stop the race and not a real one
					if (err == '!RACEDONE!') return self._execute();

					self._execute(err);
				});
				break;
			case 'deferArray':
				currentExec.payload.forEach(function(task) {
					self._deferAdd(null, task, currentExec);
				});

				redo = true;
				break;
			case 'deferObject':
				Object.keys(currentExec.payload).forEach(function(key) {
					self._deferAdd(key, currentExec.payload[key], currentExec);
				});

				redo = true;
				break;
			case 'await': // Await can operate in two modes, either payload=[] (examine all) else (examine specific keys)
				if (!currentExec.payload.length) { // Check all tasks are complete
					if (self._struct.slice(0, self._structPointer - 1).every(function(stage) { // Examine all items UP TO self one and check they are complete
						return stage.completed;
					})) { // All tasks up to self point are marked as completed
						if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
						currentExec.completed = true;
						redo = true;
					} else {
						self._structPointer--; // At least one task is outstanding - rewind to self stage so we repeat on next resolution
					}
				} else { // Check certain tasks are complete by key
					if (currentExec.payload.every(function(dep) { // Examine all named dependencies
						return !! self._context[dep];
					})) { // All are present
						if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
						currentExec.completed = true;
						redo = true;
					} else {
						self._structPointer--; // At least one dependency is outstanding - rewind to self stage so we repeat on next resolution
					}
				}
				break;
			case 'limit': // Set the options.limit variable
				self._options.limit = currentExec.payload;
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'timeout': // Set the timeout function or its timeout value

				if (currentExec.delay === false) { // Disable
					self._options.timeout = false;
				} else {
					// Set the callback if one was passed
					if (currentExec.callback) self._options.timeoutHandler = currentExec.callback;

					// Set the delay if one was passed
					if (currentExec.delay) self._options.timeout = currentExec.delay;
				}

				currentExec.completed = true;
				redo = true; // Move to next action
				break;
			case 'context': // Change the self._options.context object
				self._options.context = currentExec.payload ? currentExec.payload : self._context; // Set context (if null use internal context)
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'set': // Set a hash of variables within context
				Object.keys(currentExec.payload).forEach(function(key) {
					self._set(key, currentExec.payload[key]);
				});
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'end': // self should ALWAYS be the last item in the structure and indicates the final function call
				this._finalize();
				break;
			default:
				if (this._plugins[currentExec.type]) { // Is there a plugin that should manage this?
					this._plugins[currentExec.type].call(this, currentExec);
				} else {
					throw new Error('Unknown async-chainable exec type: ' + currentExec.type);
				}
				return;
		}
	} while (redo);
}
function _execute(err) {
	var self = this;
	if (err) return this._finalize(err); // An error has been raised - stop exec and call finalize now

	if (!self._executing) { // Never before run this object - run fire('start') and defer until it finishes
		self._executing = true;
		return self.fire.call(self, 'start', self._execute.bind(self));
	}

	do {
		var redo = false;
		if (self._structPointer >= self._struct.length) return this._finalize(err); // Nothing more to execute in struct
		self._deferCheck(); // Kick off any pending deferred items
		var currentExec = self._struct[self._structPointer];
		// Sanity checks {{{
		if (!currentExec.type) {
			throw new Error('No type is specified for async-chainable structure at offset ' + self._structPointer);
			return self;
		}
		// }}}
		self._structPointer++;

		// Skip step when function supports skipping if the argument is empty {{{
		if (
			[
				'parallelArray', 'parallelObject',
				'forEachArray', 'forEachObject',
				'seriesArray', 'seriesObject',
				'deferArray', 'deferObject',
				'set'
			].indexOf(currentExec.type) > -1 &&
			(
				!currentExec.payload || // Not set OR
				(argy.isType(currentExec.payload, 'array') && !currentExec.payload.length) || // An empty array
				(argy.isType(currentExec.payload, 'object') && !Object.keys(currentExec.payload).length) // An empty object
			)
		) {
			currentExec.completed = true;
			redo = true;
			continue;
		}
		// }}}

		switch (currentExec.type) {
			case 'forEachRange':
				var iterArray = Array(currentExec.max - currentExec.min + 1).fill(false);
				self.runArray(iterArray.map(function(v, i) {
					var val = self._context._item = currentExec.min + i;
					var index = self._context._key = i;
					return function(next) {
						if (currentExec.translate) val = currentExec.translate(val, index, iterArray.length);
						run(self._options.context, currentExec.callback, next, [val, index, iterArray.length]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachArray':
				self.runArray(currentExec.payload.map(function(item, iter) {
					self._context._item = item;
					self._context._key = iter;
					return function(next) {
						run(self._options.context, currentExec.callback, next, [item, iter]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachObject':
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						self._context._item = currentExec.payload[key];
						self._context._key = key;
						run(self._options.context, currentExec.callback, function(err, value) {
							self._set(key, value); // Allocate returned value to context
							next(err);
						}, [currentExec.payload[key], key]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'forEachLateBound':
				if (!currentExec.payload || !currentExec.payload.length) { // Payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				var resolvedPayload = self.getPath(self._context, currentExec.payload);
				if (!resolvedPayload) { // Resolved payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				// Replace own exec array with actual type of payload now we know what it is {{{
				if (argy.isType(resolvedPayload, 'array')) {
					currentExec.type = 'forEachArray';
				} else if (argy.isType(resolvedPayload, 'object')) {
					currentExec.type = 'forEachObject';
				} else {
					throw new Error('Cannot perform forEach over unknown object type: ' + argy.getType(resolvedPayload));
				}
				currentExec.payload = resolvedPayload;
				self._structPointer--; // Force re-eval of this chain item now its been replace with its real (late-bound) type
				redo = true;
				// }}}
				break;

			case 'mapArray':
				var output = new Array(currentExec.payload.length);
				self.runArray(currentExec.payload.map(function(item, iter) {
					self._context._item = item;
					self._context._key = iter;
					return function(next) {
						run(self._options.context, currentExec.callback, function(err, value) {
							if (err) return next(err);
							output[iter] = value;
							next();
						}, [item, iter]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._set(currentExec.output, output);
					self._execute(err);
				});
				break;
			case 'mapObject':
				var output = {};
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						self._context._item = currentExec.payload[key];
						self._context._key = key;
						run(self._options.context, currentExec.callback, function(err, value, outputKey) {
							output[outputKey || key] = value;
							next(err);
						}, [currentExec.payload[key], key]);
					};
				}), self._options.limit, function(err) {
					currentExec.completed = true;
					self._set(currentExec.output, output);
					self._execute(err);
				});
				break;
			case 'mapLateBound':
				if (!currentExec.payload || !currentExec.payload.length) { // Payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				var resolvedPayload = self.getPath(self._context, currentExec.payload);
				if (!resolvedPayload) { // Resolved payload is blank
					// Goto next chain
					currentExec.completed = true;
					redo = true;
					break;
				}

				// Replace own exec array with actual type of payload now we know what it is {{{
				if (argy.isType(resolvedPayload, 'array')) {
					currentExec.type = 'mapArray';
				} else if (argy.isType(resolvedPayload, 'object')) {
					currentExec.type = 'mapObject';
				} else {
					throw new Error('Cannot perform map over unknown object type: ' + argy.getType(resolvedPayload));
				}
				currentExec.payload = resolvedPayload;
				self._structPointer--; // Force re-eval of this chain item now its been replace with its real (late-bound) type
				redo = true;
				// }}}
				break;
			case 'parallelArray':
			case 'seriesArray':
				self.runArray(currentExec.payload.map(function(task) {
					return function(next) {
						run(self._options.context, task, next);
					};
				}), currentExec.type == 'parallelArray' ? self._options.limit : 1, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'seriesObject':
			case 'parallelObject':
				self.runArray(Object.keys(currentExec.payload).map(function(key) {
					return function(next) {
						run(self._options.context, currentExec.payload[key], function(err, value) {
							self._set(key, value); // Allocate returned value to context
							next(err);
						});
					};
				}), currentExec.type == 'parallelObject' ? self._options.limit : 1, function(err) {
					currentExec.completed = true;
					self._execute(err);
				});
				break;
			case 'race':
				var hasResult = false;
				var hasError = false;
				self.runArray(currentExec.payload.map(function(task) {
					return function(next) {
						run(self._options.context, task, function(err, taskResult) {
							if (err) {
								hasError = true
								next(err, taskResult);
							} else if (!hasResult && !hasError && taskResult !== null && typeof taskResult !== 'undefined') {
								self._set(currentExec.id, taskResult); // Allocate returned value to context
								hasResult = true;
								next('!RACEDONE!', taskResult); // Force an error to stop the run() function
							} else {
								next(err, taskResult);
							}
						});
					};
				}), self._options.limit, function(err, val) {
					currentExec.completed = true;

					// Override race finish error as it was just to stop the race and not a real one
					if (err == '!RACEDONE!') return self._execute();

					self._execute(err);
				});
				break;
			case 'deferArray':
				currentExec.payload.forEach(function(task) {
					self._deferAdd(null, task, currentExec);
				});

				redo = true;
				break;
			case 'deferObject':
				Object.keys(currentExec.payload).forEach(function(key) {
					self._deferAdd(key, currentExec.payload[key], currentExec);
				});

				redo = true;
				break;
			case 'await': // Await can operate in two modes, either payload=[] (examine all) else (examine specific keys)
				if (!currentExec.payload.length) { // Check all tasks are complete
					if (self._struct.slice(0, self._structPointer - 1).every(function(stage) { // Examine all items UP TO self one and check they are complete
						return stage.completed;
					})) { // All tasks up to self point are marked as completed
						if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
						currentExec.completed = true;
						redo = true;
					} else {
						self._structPointer--; // At least one task is outstanding - rewind to self stage so we repeat on next resolution
					}
				} else { // Check certain tasks are complete by key
					if (currentExec.payload.every(function(dep) { // Examine all named dependencies
						return !! self._context[dep];
					})) { // All are present
						if (_deferTimeoutHandle) clearTimeout(_deferTimeoutHandle);
						currentExec.completed = true;
						redo = true;
					} else {
						self._structPointer--; // At least one dependency is outstanding - rewind to self stage so we repeat on next resolution
					}
				}
				break;
			case 'limit': // Set the options.limit variable
				self._options.limit = currentExec.payload;
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'timeout': // Set the timeout function or its timeout value

				if (currentExec.delay === false) { // Disable
					self._options.timeout = false;
				} else {
					// Set the callback if one was passed
					if (currentExec.callback) self._options.timeoutHandler = currentExec.callback;

					// Set the delay if one was passed
					if (currentExec.delay) self._options.timeout = currentExec.delay;
				}

				currentExec.completed = true;
				redo = true; // Move to next action
				break;
			case 'context': // Change the self._options.context object
				self._options.context = currentExec.payload ? currentExec.payload : self._context; // Set context (if null use internal context)
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'set': // Set a hash of variables within context
				Object.keys(currentExec.payload).forEach(function(key) {
					self._set(key, currentExec.payload[key]);
				});
				currentExec.completed = true;
				redo = true; // Move on to next action
				break;
			case 'end': // self should ALWAYS be the last item in the structure and indicates the final function call
				this._finalize();
				break;
			default:
				if (this._plugins[currentExec.type]) { // Is there a plugin that should manage this?
					this._plugins[currentExec.type].call(this, currentExec);
				} else {
					throw new Error('Unknown async-chainable exec type: ' + currentExec.type);
				}
				return;
		}
	} while (redo);
}
Internal function to execute the next pending queue item
This is usually called after the completion of every asyncChainable.run call
@access private