237 lines
7.4 KiB
JavaScript
237 lines
7.4 KiB
JavaScript
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;
|