var _ = require('lodash'), async = require('async'), backpack = require('../backpack'), Instruction = require('./instruction'), Run; // constructor /** * The run object is the primary way to interact with a run in progress. It allows controlling the run (pausing, * starting, etc) and holds references to the helpers, such as requesters and authorizer. * * @param state * @param options * * @property {Requester} requester * @constructor */ Run = function PostmanCollectionRun (state, options) { // eslint-disable-line func-name-matching _.assign(this, /** @lends Run.prototype */ { /** * @private * @type {Object} * @todo: state also holds the host for now (if any). */ state: _.assign({}, state), /** * @private * @type {InstructionPool} */ pool: Instruction.pool(Run.commands), /** * @private * @type {Object} */ stack: {}, /** * @private * @type {Object} */ options: options || {} }); }; _.assign(Run.prototype, { // eslint-disable-next-line jsdoc/check-param-names /** * @param {String} action * @param {Object} [payload] * @param {*} [args...] */ queue: function (action, payload) { // extract the arguments that are to be forwarded to the processor return this._schedule(action, payload, _.slice(arguments, 2), false); }, // eslint-disable-next-line jsdoc/check-param-names /** * @param {String} action * @param {Object} [payload] * @param {*} [args...] */ interrupt: function (action, payload) { // extract the arguments that are to be forwarded to the processor return this._schedule(action, payload, _.slice(arguments, 2), true); }, // eslint-disable-next-line jsdoc/check-param-names /** * Suspends current instruction and executes the given instruction. * * This method explicitly chooses not to handle errors, to allow the caller to catch errors and continue execution * without terminating the instruction queue. However, it is up to the caller to make sure errors are handled, * or it will go unhandled. * * @param {String} action * @param {Object} payload * @param {*} [args...] */ immediate: function (action, payload) { var scope = this, instruction = this.pool.create(action, payload, _.slice(arguments, 2)); // we directly execute this instruction instead od queueing it. setTimeout(function () { // we do not have callback, hence we send _.noop. we could have had made callback in .execute optional, but // that would suppress design-time bugs in majority use-case and hence we avoided the same. instruction.execute(_.noop, scope); }, 0); return instruction; }, /** * @param {Function|Object} callback */ start: function (callback) { // @todo add `when` parameter to backpack.normalise callback = backpack.normalise(callback, Object.keys(Run.triggers)); // cannot start run if it is already running if (this.triggers) { return callback(new Error('run: already running')); } var timeback = callback; if (_.isFinite(_.get(this.options, 'timeout.global'))) { timeback = backpack.timeback(callback, this.options.timeout.global, this, function () { this.pool.clear(); }); } // invoke all the initialiser functions one after another and if it has any error then abort with callback. async.series(_.map(Run.initialisers, function (initializer) { return initializer.bind(this); }.bind(this)), function (err) { if (err) { return callback(err); } // save the normalised callbacks as triggers this.triggers = callback; this.triggers.start(null, this.state.cursor.current()); // @todo may throw error if cursor absent this._process(timeback); }.bind(this)); }, /** * @private * @param {Object|Cursor} cursor * @return {Item} */ resolveCursor: function (cursor) { if (!cursor || !Array.isArray(this.state.items)) { return; } return this.state.items[cursor.position]; }, /** * @private * * @param {String} action * @param {Object} [payload] * @param {Array} [args] * @param {Boolean} [immediate] */ _schedule: function (action, payload, args, immediate) { var instruction = this.pool.create(action, payload, args); // based on whether the immediate flag is set, add to the top or bottom of the instruction queue. (immediate ? this.pool.unshift : this.pool.push)(instruction); return instruction; }, _process: function (callback) { // extract the command from the queue var instruction = this.pool.shift(); // if there is nothing to process, exit if (!instruction) { callback(null, this.state.cursor.current()); return; } instruction.execute(function (err) { return err ? callback(err, this.state.cursor.current()) : this._process(callback); // process recursively }, this); } }); _.assign(Run, { /** * Stores all events that runner triggers * * @type {Object} */ triggers: { start: true }, /** * stores all execution commands * @enum {Function} * * @note commands are loaded by flattening the modules in the `./commands` directory */ commands: {}, /** * Functions executed with commands on start * @type {Array} */ initialisers: [] }); // commands are loaded by flattening the modules in the `./commands` directory Run.commands = _.transform({ 'control.command': require('./extensions/control.command'), 'event.command': require('./extensions/event.command'), 'httprequest.command': require('./extensions/http-request.command'), 'request.command': require('./extensions/request.command'), 'waterfall.command': require('./extensions/waterfall.command'), 'item.command': require('./extensions/item.command'), 'delay.command': require('./extensions/delay.command') }, function (all, extension) { // extract the prototype from the command interface _.has(extension, 'prototype') && _.forOwn(extension.prototype, function (value, prop) { if (Run.prototype.hasOwnProperty(prop)) { throw new Error('run: duplicate command prototype extension ' + prop); } Run.prototype[prop] = value; }); // put the triggers in a box _.has(extension, 'triggers') && _.isArray(extension.triggers) && _.forEach(extension.triggers, function (name) { name && (Run.triggers[name] = true); }); // we add the processors to the processor list _.has(extension, 'process') && _.forOwn(extension.process, function (command, name) { if (!_.isFunction(command)) { return; } if (all.hasOwnProperty(name)) { throw new Error('run: duplicate command processor ' + name); } // finally add the command function to the accumulator all[name] = command; }); // add the initialisation functions _.has(extension, 'init') && _.isFunction(extension.init) && Run.initialisers.push(extension.init); }); module.exports = Run;