dataflo.ws
Version:
Zero-code JSON config-based dataflow engine for Node, PhoneGap and browser.
682 lines (552 loc) • 16.9 kB
JavaScript
var define;
if (typeof define === "undefined")
define = function (classInstance) {
classInstance (require, exports, module);
}
define (function (require, exports, module) {
var EventEmitter = require ('events').EventEmitter,
util = require ('util'),
common = require ('../common');
var taskStateList = [
'scarce', 'ready', 'running', 'idle',
'complete', 'failed', 'skipped'
];
var taskStateNames = {};
var taskStateMethods = {};
for (var stateNum = 0; stateNum < taskStateList.length; stateNum++) {
taskStateNames[taskStateList[stateNum]] = stateNum;
var fName = 'is' + taskStateList[stateNum].toLowerCase().replace(
/\b([a-z])/i, function(c) {return c.toUpperCase()}
);
taskStateMethods[fName] = function (x) {
return function () {return this.state == x};
} (stateNum);
}
/**
* @class task.task
* @extends events.EventEmitter
*
* Tasks are atomic synchronous/asynchronous entities that configure
* what must be done, what prerequisitives must be satisfied before doing it,
* and, optionally, where to store the task's result.
*
* `task` is an abstract base class that specific task types
* should inherite from.
*
* The base `task` class provides methods to control the task execution
* externally. These methods are called by the dataflow.
* They cycle through a number of states ({@link #stateNames})
* and emit events.
*
* ### Example
*
* A sequence of task configs
* within the *dataflo.ws* concept. `task` objects are instantiated
* by `dataflow` internally.
*
* @cfg {String} className (required) The name of a module-exported class
* to be instantiated as an asynchronous task.
*
* **Warning**: Params {@link #className} and {@link #functionName}
* are mutually exclusive.
*
* @cfg {String} functionName (required) The name of a module-exported function
* to be called as a synchronous task.
*
* **Warning**: Params {@link #className} and {@link #functionName}
* are mutually exclusive.
*
* @cfg {String} [method="run"] The entry point method name.
* This method will be called after the requirements are satisfied (if any).
*
* @cfg {Number} [retries=0] The number of times to retry to run the task.
*
* @cfg {Number} [timeout=1] The number of seconds between retries
* to run the task.
*
* @cfg {String} produce The name of the dataflow data field to receive
* the result of the task.
*
* @cfg {Function|String|String[]} require Lists requirements to check.
*
* Implementations might provide either a function that checks whether
* the requirements are satisfied or an identifier or a list of identifiers,
* representing required objects.
*
* @returns {Boolean} If `require` is callable, it must return a boolean value.
*
* @cfg {Boolean} important If the task is marked important,
* it may declare itself {@link #failed}.
* Used in custom {@link #method} methods.
*/
var task = module.exports = function (config) {
}
util.inherits (task, EventEmitter);
util.extend (task.prototype, taskStateMethods, {
_launch: function () {
//this.emit ('log', 'RUN RETRIES : ' + this.retries);
if (this.state != 1) return;
this.state = 2;
var method = this[this.method || 'run'];
if (!method) {
this.state = 5;
this.emit ('error', 'no method named "' + (this.method || 'run') + "\" in current task's class");
return;
}
method.call (this);
},
/**
* @method _cancel
* Cancels the running task. The task is registered as attempted
* to run.
*
* Switches the task's state from `running` to `idle`.
*
* If the {@link #retries} limit allows, attempt to run the task
* after a delay ({@link #timeout}).
*
* Publishes {@link #event-cancel}.
*/
_cancel: function (value) {
this.attempts ++;
if (this.state == 2) return;
this.state = 5;
if (this.cancel) this.cancel.apply (arguments);
this.clearOperationTimeout();
//this.emit ('log', 'CANCEL RETRIES : ' + this.retries);
if (this.attempts - this.retries - 1 < 0) {
this.state = 1;
var method = this[this.method || 'run'];
setTimeout (method.bind(this), this.timeout);
}
/**
* @event cancel
* Published on task cancel.
*/
this.emit ('cancel', value);
},
init: function (config) {
this.require = config.require || null;
this.mustProduce = config.mustProduce;
this.cb = config.cb;
this.cbScope = config.cbScope;
this.className = config.className;
this.functionName = config.functionName;
this.originalConfig = config.originalConfig;
this.flowId = config.flowId;
this.getDict = config.getDict;
this.method = config.method;
if (this.className && !this.method)
this.method = 'run';
if (!this.logTitle) {
if (this.className) {
this.logTitle = this.className + '.' + this.method;
} else {
this.logTitle = this.functionName;
}
}
var stateList = taskStateList;
var self = this;
this.state = 0;
// default values
// TODO: this is provided only on run
self.timeout = config.timeout || 10000;
self.retries = config.retries || null;
self.attempts = 0;
self.important = config.important || void 0;
// `DEFAULT_CONFIG' is a formal config specification + default values
if (self.DEFAULT_CONFIG) {
util.shallowMerge(self, self.DEFAULT_CONFIG);
}
var state = this.checkState ();
// console.log (this.url, 'state is', stateList[state], ' (' + state + ')', (state == 0 ? (this.require instanceof Array ? this.require.join (', ') : this.require) : ''));
},
/**
* @method completed
* Publishes {@link #event-complete} with the result object
* that will go into the {@link #produce} field of the dataflow.
*
* @param {Object} result The product of the task.
*/
completed: function (result) {
this.state = 4;
var mustProduce = this.mustProduce;
if (mustProduce) {
var checkString = (mustProduce instanceof Array ? mustProduce.join (' && ') : mustProduce);
var satisfy = 0;
try {satisfy = eval ("if ("+ checkString +") 1") } catch (e) {};
if (!satisfy) {
// TODO: WebApp.Loader.instance.taskError (this);
console.error ("task " + this.url + " must produce " + checkString + " but it doesn't");
// TODO: return;
}
}
// coroutine call
if (typeof this.cb == 'function') {
// console.log ('cb defined', this.cb, this.cbScope);
if (this.cbScope) {
this.cb.call (this.cbScope, this);
} else {
this.cb (this);
}
}
//@behrad set $empty on completion of all task types
if (common.isEmpty (result)) {
this.empty();
}
/**
* @event complete
* Published upon task completion.
*
* @param {task.task} task
* @param {Object} result
*/
this.emit ("complete", this, result);
},
/**
* @method skipped
* Skips the task with a given result.
*
* Publishes {@link #event-skip}.
*
* @param {Object} result Substitutes the tasks's complete result.
*/
skipped: function (result) {
this.state = 6;
/**
* @event skip
* Triggered when the task is {@link #skipped}.
* @param {task.task} task
* @param {Object} result
*/
this.emit ("skip", this, result);
},
/**
* @method empty
* Run when the task has been completed correctly,
* but the result is a non-value (null or empty).
*
* Publishes {@link #event-empty}.
*
*/
empty: function () {
this.state = 6; // completed
this.emit('empty', this);
},
/**
* @method mapFields
* Translates task configuration from custom field-naming cheme.
*
* @param {Object} item
*/
mapFields: function (item) {
var self = this;
for (var k in self.mapping) {
if (item[self.mapping[k]])
item[k] = item[self.mapping[k]];
}
return item;
},
/**
* @method checkState
* Checks requirements and updates the task state.
*
* @return {Number} The new state code.
*/
checkState: function () {
var self = this;
if (!self.require && this.state == 0) {
this.state = 1;
}
if (this.state >= 1)
return this.state;
var satisfy = 0;
if (typeof self.require == 'function') {
satisfy = self.require ();
} else {
try {
satisfy = eval ("if ("+ (
self.require instanceof Array
? self.require.join (' && ')
: self.require)+") 1")
} catch (e) {
};
}
if (satisfy) {
this.state = 1;
return this.state;
}
return this.state;
},
/**
* @private
*/
clearOperationTimeout: function() {
if (this.timeoutId) {
clearTimeout (this.timeoutId);
this.timeoutId = 0;
}
},
/**
* @private
*/
// WTF??? MODEL???
activityCheck: function (place, breakOnly) {
if (place!=="model.fetch data") {
// console.log("%%%%%%%%%%%%%place -> ", place);
}
var self = this;
if (breakOnly === void (0)) {
breakOnly = false;
}
self.clearOperationTimeout();
if (!breakOnly)
{
self.timeoutId = setTimeout(function () {
self.state = 5;
self.emit (
'log', 'timeout is over for ' + place + ' operation'
);
self.model.stop();
self._cancel();
}, self.timeout);
}
},
/**
* @enum stateNames
*
* A map of the task state codes to human-readable state descriptions.
*
* The states codes are:
*
* - `scarce`
* - `ready`
* - `running`
* - `idle`
* - `complete`
* - `failed`
* - `skipped`
* - `empty`
*/
stateNames: taskStateNames,
/**
* @method failed
* Emits an {@link #event-error}.
*
* Cancels (calls {@link #method-cancel}) the task if it was ready
* or running; or just emits {@link #event-cancel} if not.
*
* Sets the status to `failed`.
*
* Sidenote: when a task fails the whole dataflow, that it belongs to, fails.
*
* @return {Boolean} Always true.
* @param {Error} Error object.
*/
failed: function (e, data) {
var prevState = this.state;
this.state = 5;
/**
* @event error
* Emitted on task fail and on internal errors.
* @param {Error} e Error object.
*/
this.emit('error', e);
// if task failed at scarce state
if (prevState)
this._cancel (data || e)
else
this.emit ('cancel', data || e);
return true;
}
});
/**
* @method prepare
* Prepare task class to run, handle errors, workaround for a $every tasks.
*
* @return {Task}.
* @param {Flow} flow for task.
* @param {DataFlows} dataflows object.
* @param {Function} generator for params dictionary and check requirements.
* @param {Integer} index in task array for that flow.
* @param {Array} task array.
*/
task.prepare = function (flow, dataflows, gen, taskParams, idx, array) {
var theTask;
var idxLog = (idx < 10 ? " " : "") + idx;
if ($isServerSide) {
idxLog = "\x1B[0;3" + (parseInt(idx) % 8) + "m" + idxLog + "\x1B[0m";
}
var actualTaskParams = {
dfTaskNo: idx,
dfTaskLogNum: idxLog
};
var taskTemplateName = taskParams.$template;
if (taskTemplateName && flow.templates && flow.templates[taskTemplateName]) {
util.extend (true, actualTaskParams, flow.templates[taskTemplateName]);
delete actualTaskParams.$template;
}
// we expand templates in every place in config
// for tasks such as every
util.extend (true, actualTaskParams, taskParams);
if (actualTaskParams.$every) {
actualTaskParams.$class = 'every';
if (!actualTaskParams.$tasks) {
flow.logError ('missing $tasks property for $every task');
flow.ready = false;
return;
}
actualTaskParams.$tasks.forEach (function (everyTaskConf, idx) {
var taskTemplateName = everyTaskConf.$template;
if (taskTemplateName && flow.templates && flow.templates[taskTemplateName]) {
var newEveryTaskConf = util.extend (true, {}, flow.templates[taskTemplateName]);
util.extend (true, newEveryTaskConf, everyTaskConf);
util.extend (true, everyTaskConf, newEveryTaskConf);
delete everyTaskConf.$template;
console.log (everyTaskConf, actualTaskParams.$tasks[idx]);//everyTaskConf.$tasks
}
});
}
// var originalTaskConfig = JSON.parse(JSON.stringify(actualTaskParams));
var originalTaskConfig = util.extend (true, {}, actualTaskParams);
// check for data persistence in flow.templates[taskTemplateName], taskParams
// console.log (taskParams);
var taskClassName = actualTaskParams.className || actualTaskParams.$class || actualTaskParams.task || actualTaskParams.task;
var taskFnName = actualTaskParams.functionName || actualTaskParams.$function;
var taskPromise = actualTaskParams.promise || actualTaskParams.$promise;
var taskErrBack = actualTaskParams.errback || actualTaskParams.$errback;
// console.log ('task:', taskClassName, 'function:', taskFnName, 'promise:', taskPromise, 'errback:', taskErrBack);
if (taskClassName && taskFnName)
flow.logError ('defined both className and functionName, using className');
if (taskClassName) {
var xTaskClass;
xTaskClass = dataflows.task (taskClassName);
try {
theTask = new xTaskClass ({
originalConfig: originalTaskConfig,
className: taskClassName,
method: actualTaskParams.method || actualTaskParams.$method,
require: gen ('checkRequirements', actualTaskParams),
important: actualTaskParams.important || actualTaskParams.$important,
flowId: flow.coloredId,
getDict: gen ('createDict'),
timeout: actualTaskParams.timeout
});
} catch (e) {
flow.logError ('instance of "'+taskClassName+'" creation failed:');
flow.logError (e.stack);
throw ('instance of "'+taskClassName+'" creation failed:');
flow.ready = false;
}
} else if (taskFnName || taskPromise || taskErrBack) {
// flow.log ((taskParams.functionName || taskParams.logTitle) + ': initializing task from function');
var xTaskClass = function (config) {
this.init (config);
};
if (taskPromise) {
// functions and promises similar, but function return value, promise promisepromise promise
taskFnName = taskPromise;
}
if (taskErrBack) {
// functions and promises similar, but function return value, promise promisepromise promise
taskFnName = taskErrBack;
}
util.inherits (xTaskClass, task);
util.extend (xTaskClass.prototype, {
run: function () {
var failed = false;
/**
* Apply $function to $args in $scope.
*/
var origin = null;
// WTF???
var fnPath = taskFnName.split('#', 2);
if (fnPath.length == 2 && $global.project) {
origin = $global.project.require(fnPath[0]);
taskFnName = fnPath[1];
} else if (this.$origin) {
origin = this.$origin;
} else {
origin = $global.$mainModule.exports;
}
var method;
method = common.getByPath (taskFnName, origin);
/**
* Try to look up $function in the global scope.
*/
if (!method || 'function' !== typeof method.value) {
method = common.getByPath(taskFnName);
}
if (!method || 'function' !== typeof method.value) {
failed = taskFnName + ' is not a function';
this.failed(failed);
}
var fn = method.value;
var ctx = this.$scope || method.scope;
var args = this.$args;
var argsType = Object.typeOf(args);
if (null == args) {
args = [ this ];
} else if (
'Array' != argsType &&
'Arguments' != argsType
) {
args = [ args ];
}
// console.log ('task:', taskClassName, 'function:', taskFnName, 'promise:', taskPromise, 'errback:', taskErrBack);
if (taskErrBack) {
args.push ((function (err) {
var cbArgs = [].slice.call (arguments, 1);
if (err) {
this.failed.apply (this, arguments);
};
this.completed.apply (this, cbArgs);
}).bind(this));
}
try {
var returnVal = fn.apply(ctx, args);
} catch (e) {
failed = e;
this.failed(failed);
}
if (taskPromise) {
returnVal.then (
this.completed.bind (this),
this.failed.bind (this)
);
} else if (taskErrBack) {
} else if (failed) {
throw failed;
} else {
this.completed(returnVal);
// if (isVoid(returnVal)) {
// if (common.isEmpty(returnVal)) {
// this.empty();
// }
}
}
});
theTask = new xTaskClass ({
originalConfig: originalTaskConfig,
functionName: taskFnName || taskPromise || taskErrBack,
logTitle: actualTaskParams.logTitle || actualTaskParams.$logTitle || actualTaskParams.displayName,
require: gen ('checkRequirements', actualTaskParams),
important: actualTaskParams.important || actualTaskParams.$important,
timeout: actualTaskParams.timeout
});
} else {
flow.logError ("cannot create task from structure:\n", taskParams);
flow.logError ('you must define $task, $function or $promise field');
// TODO: return something
}
// console.log (task);
return theTask;
};
/**
* @method EmitError
* Implementation-specific.
* When an unexpected error occurs, the task is automatically {@link #failed}.
*/
task.prototype.EmitError = task.prototype.failed;
return task;
});