UNPKG

dataflo.ws

Version:

Zero-code JSON config-based dataflow engine for Node, PhoneGap and browser.

549 lines (440 loc) 13.9 kB
var EventEmitter = require ('events').EventEmitter, util = require ('util'), dataflows = require ('./index'), common = dataflows.common, taskClass = require ('./task/base'), paint = dataflows.color, tokenInitiator; var $global = common.$global; var taskStateNames = taskClass.prototype.stateNames; function isVoid(val) { return void 0 == val; } function taskRequirements (requirements, dict) { var result = []; for (var k in requirements) { var requirement = requirements[k]; for (var i = 0; i < requirement.length; i++) { try { if (isVoid (common.pathToVal (dict, requirement[i]))) result.push (k); } catch (e) { result.push (k); } } } return result; } function checkTaskParams (params, dict, prefix, marks) { // parse task params // TODO: modify this function because recursive changes of parameters works dirty (indexOf for value) var AllowedValueTypes = { Boolean: true, Number: true, Function: true, Date: true }; if (prefix == void 0) prefix = ''; if (prefix) prefix += '.'; var modifiedParams; var failedParams = []; if (Object.is('Array', params)) { // params is array modifiedParams = []; params.forEach(function (val, index, arr) { if (Object.is('String', val)) { // string try { var tmp = val.interpolate(dict, marks); if (tmp === undefined) { failedParams.push (prefix+'['+index+']'); } else { modifiedParams.push(tmp); } } catch (e) { failedParams.push (prefix+'['+index+']'); } } else if (Object.typeOf(val) in AllowedValueTypes) { modifiedParams.push(val); } else { var result = checkTaskParams( val, dict, prefix+'['+index+']', marks ); modifiedParams.push(result.modified); failedParams = failedParams.concat (result.failed); } }); } else { // params is hash modifiedParams = {}; Object.keys(params).forEach(function (key) { var val = params[key]; var valCheck = val; if (Object.is('String', val)) { try { var tmp = val.interpolate(dict, marks); if (tmp === undefined) { failedParams.push (prefix+key); } else { modifiedParams[key] = tmp; } } catch (e) { //console.error('ERR!'); failedParams.push (prefix+key); } } else if (Object.typeOf(val) in AllowedValueTypes) { modifiedParams[key] = val; } else { // val is hash || array var result = checkTaskParams(val, dict, prefix+key, marks); modifiedParams[key] = result.modified; failedParams = failedParams.concat (result.failed); } }); } return { modified: modifiedParams, failed: failedParams || [] }; } /** * @class flow * @extends events.EventEmitter * * The heart of the framework. Parses task configurations, loads dependencies, * launches tasks, stores their result. When all tasks are completed, * notifies subscribers (inititators). * * @cfg {Object} config (required) dataflow configuration. * @cfg {String} config.$class (required) Class to instantiate * (alias of config.className). * @cfg {String} config.$function (required) Synchronous function to be run * (instead of a class). Alias of functionName. * @cfg {String} config.$set Path to the property in which the produced data * will be stored. * @cfg {String} config.$method Method to be run after the class instantiation. * @cfg {Object} reqParam (required) dataflow parameters. */ var dataflow = module.exports = function (config, reqParam) { var self = this; // TODO: copy only required things util.extend (true, this, config); // this is immutable config skeleton util.extend (true, this, reqParam); // this is config fixup this.created = new Date().getTime(); // here we make sure dataflow uid generated // TODO: check for cpu load var salt = (Math.random () * 1e6).toFixed(0); this.id = this.id || (this.started ^ salt) % 1e6; if (!this.idPrefix) this.idPrefix = ''; if (!this.stage) this.stage = 'dataflow'; //if (!this.stageMarkers[this.stage]) // console.error ('there is no such stage marker: ' + this.stage); var idString = ""+this.id; while (idString.length < 6) {idString = '0' + idString}; this.coloredId = [ "" + idString[0] + idString[1], "" + idString[2] + idString[3], "" + idString[4] + idString[5] ].map (function (item) { if ($isServerSide) return "\x1B[0;3" + (parseInt(item) % 8) + "m" + item + "\x1B[0m"; return item; }).join (''); this.data = this.data || { data: {} }; // console.log ('!!!!!!!!!!!!!!!!!!!' + this.data.keys.length); // console.log ('config, reqParam', config, reqParam); self.ready = true; var tasks = config.tasks; // TODO: optimize usage - find placeholders and check only placeholders if (config.tasksFrom) { if (!tokenInitiator) tokenInitiator = require ('initiator/token'); var flowByToken; if ( !project.config.initiator || !project.config.initiator.token || !project.config.initiator.token.flows || !(flowByToken = project.config.initiator.token.flows[config.tasksFrom]) || !flowByToken.tasks ) { this.log ('"tasksFrom" parameter requires to have "initiator/token/flows'+config.tasksFrom+'" configuration in project'); this.ready = false; } tasks = flowByToken.tasks; } else if (!config.tasks || !config.tasks.length) { config.tasks = []; } function createDict () { // TODO: very bad idea: reqParam overwrites flow.data var dict = util.extend (true, self.data, reqParam); dict.global = $global; dict.appMain = $mainModule.exports; if ($isServerSide) { try { dict.project = project; } catch (e) {} } return dict; } var taskGen = function (type, actualTaskParams) { if (type === 'createDict') return createDict; if (type === 'checkRequirements') return function () { var dict = createDict (); var result = checkTaskParams (actualTaskParams, dict, self.marks); if (result.failed && result.failed.length > 0) { this.unsatisfiedRequirements = result.failed; return false; } else if (result.modified) { // TODO: bad util.extend (this, result.modified); return true; } } } this.tasks = tasks.map (taskClass.prepare.bind (taskClass, self, dataflows, taskGen)); }; util.inherits (dataflow, EventEmitter); function pad(n) { return n < 10 ? '0' + n.toString(10) : n.toString(10); } // one second low resolution timer Date.dataflowsLowRes = new Date (); Date.dataflowsLowResInterval = setInterval (function () { Date.dataflowsLowRes = new Date (); }, 1000); function timestamp () { var lowRes = Date.dataflowsLowRes; var time = [ pad(lowRes.getHours()), pad(lowRes.getMinutes()), pad(lowRes.getSeconds()) ].join(':'); var date = [ lowRes.getFullYear(), pad(lowRes.getMonth() + 1), pad(lowRes.getDate()) ].join ('-'); return [date, time].join(' '); } util.extend (dataflow.prototype, { checkTaskParams: checkTaskParams, taskRequirements: taskRequirements, failed: false, isIdle: true, haveCompletedTasks: false, /** * @method run Initiators call this method to launch the dataflow. */ runDelayed: function () { var self = this; if ($isClientSide) { setTimeout (function () {self.run ();}, 0); } else if ($isServerSide) { process.nextTick (function () {self.run ()}); } }, run: function () { if (!this.started) this.started = new Date().getTime(); var self = this; if (self.stopped) return; /* @behrad following was overriding already set failed status by failed tasks */ // self.failed = false; self.isIdle = false; self.haveCompletedTasks = false; // self.log ('dataflow run'); this.taskStates = [0, 0, 0, 0, 0, 0, 0]; // check task states if (!this.tasks) { self.emit ('failed', self); self.logError (this.stage + ' failed immediately due empty task list'); self.isIdle = true; return; } if (!this.ready) { self.emit ('failed', self); self.logError (this.stage + ' failed immediately due unready state'); self.isIdle = true; return; } this.tasks.forEach (function (task, idx) { if (task.subscribed === void(0)) { self.addEventListenersToTask (task); } task.checkState (); self.taskStates[task.state]++; // console.log ('task.className, task.state\n', task, task.state, task.isReady ()); if (task.isReady ()) { self.logTask (task, 'started'); try { task._launch (); } catch (e) { task.failed (e); // self.logTaskError (task, 'failed to run', e); } // sync task support if (!task.isReady()) { self.taskStates[task.stateNames.ready]--; self.taskStates[task.state]++; } } }); var taskStateNames = taskClass.prototype.stateNames; if (this.taskStates[taskStateNames.ready] || this.taskStates[taskStateNames.running]) { // it is save to continue, wait for running/ready task // console.log ('have running tasks'); self.isIdle = true; return; } else if (self.haveCompletedTasks) { // console.log ('have completed tasks'); // stack will be happy self.runDelayed(); self.isIdle = true; return; } self.stopped = new Date().getTime(); var scarceTaskMessage = 'unsatisfied requirements: '; // TODO: display scarce tasks unsatisfied requirements if (this.taskStates[taskStateNames.scarce]) { self.tasks.map (function (task, idx) { if (task.state != taskStateNames.scarce && task.state != taskStateNames.skipped) return; if (task.important) { task.failed (idx + " important task didn't start"); self.taskStates[taskStateNames.scarce]--; self.taskStates[task.state]++; self.failed = true; scarceTaskMessage += '(important) '; } if (task.state == taskStateNames.scarce || task.state == taskStateNames.failed) scarceTaskMessage += idx + ' ' + (task.logTitle) + ' => ' + task.unsatisfiedRequirements.join (', ') + '; '; }); self.log (scarceTaskMessage); } if (self.verbose) { var requestDump = '???'; try { requestDump = JSON.stringify (self.request) } catch (e) { if ((""+e).match (/circular/)) requestDump = 'CIRCULAR' else requestDump = e }; } if (this.failed) { // dataflow stopped and failed self.emit ('failed', self); var failedtasksCount = this.taskStates[taskStateNames.failed] self.logError (this.stage + ' failed in ' + (self.stopped - self.started) + 'ms; failed ' + failedtasksCount + ' ' + (failedtasksCount == 1 ? 'task': 'tasks') +' out of ' + self.tasks.length); } else { // dataflow stopped and not failed self.emit ('completed', self); self.log (this.stage + ' complete in ' + (self.stopped - self.started) + 'ms'); } self.isIdle = true; }, stageMarker: {prepare: "[]", dataflow: "()", presentation: "{}"}, _log: function (level, msg) { // if (this.quiet || process.quiet) return; var toLog = [].slice.call (arguments); var level = toLog.shift() || 'log'; toLog.unshift ( timestamp (), this.stageMarker[this.stage][0] + this.idPrefix + this.coloredId + this.stageMarker[this.stage][1] ); // TODO: also check for bad clients (like ie9) if ($isPhoneGap) { toLog.shift(); toLog = [toLog.join (' ')]; } console[level].apply (console, toLog); }, log: function () { var args = [].slice.call (arguments); args.unshift ('log'); this._log.apply (this, args); }, logTask: function (task, msg) { this._log ('log', task.dfTaskLogNum, task.logTitle, "("+task.state+")", msg); }, logTaskError: function (task, msg, options) { var lastFrame = ''; if (options && options.stack) { var frames = options.stack.split('\n'); var len = frames.length; if (frames.length > 1) { lastFrame = frames[1].trim(); } } this._log ( 'error', task.dfTaskLogNum, task.logTitle, '(' + task.state + ') ', paint.error ( util.inspect (msg).replace (/(^'|'$)/g, "").replace (/\\'/, "'"), util.inspect (options || '').replace (/(^'|'$)/g, "").replace (/\\'/, "'") ), lastFrame ); }, logError: function (msg, options) { // TODO: fix by using console.error this._log ('error', paint.error ( util.inspect (msg).replace (/(^'|'$)/g, "").replace (/\\'/, "'"), util.inspect (options || '').replace (/(^'|'$)/g, "").replace (/\\'/, "'") )); }, addEventListenersToTask: function (task) { var self = this; task.subscribed = 1; // loggers task.on ('log', function (message) { self.logTask (task, message); }); task.on ('warn', function (message) { self.logTaskError (task, message); }); task.on ('error', function (e) { self.error = e; self.logTaskError (task, 'error: ', e); }); // states task.on ('skip', function () { // if (task.important) { // self.failed = true; // return self.logTaskError (task, 'error ' + arguments[0]); // } self.logTask (task, 'task skipped'); if (self.isIdle) self.runDelayed (); }); task.on ('cancel', function (failedValue) { if (task.retries !== null) self.logTaskError (task, 'canceled, retries = ' + task.retries); if (!task.retries && task.$setOnFail) { common.pathToVal(self.data, task.$setOnFail, failedValue || true); self.haveCompletedTasks = true; } else { self.failed = true; } if (self.isIdle) self.runDelayed (); }); task.on ('complete', function (t, result) { if (result) { if (t.produce || t.$set) { common.pathToVal (self.data, t.produce || t.$set, result); } else if (t.$mergeWith) { common.pathToVal (self.data, t.$mergeWith, result, common.mergeObjects); } } self.logTask (task, 'task completed'); if (self.isIdle) { self.runDelayed (); } else self.haveCompletedTasks = true; }); task.on('empty', function (t) { if (t.$empty || t.$setOnEmpty) { common.pathToVal(self.data, t.$empty || t.$setOnEmpty, true); } }); } }); // legacy dataflow.isEmpty = common.isEmpty;