UNPKG

dataflo.ws

Version:

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

673 lines (541 loc) 16.5 kB
"use strict"; var EventEmitter = require ('events').EventEmitter, http = require ('http'), util = require ('util'), url = require ('url'), path = require ('path'), os = require ('os'), dataflows = require ('../index'), flow = require ('../flow'), common = dataflows.common, paint = dataflows.color; var mime, memoize; try { mime = require ('mime'); } catch (e) { console.error (paint.error ('cannot find mime module')); } try { memoize = require ('memoizee'); } catch (e) { console.error ('memoizee module not found. it provide optimized path lookups'); } /** * @class initiator.httpdi * @extends events.EventEmitter * * Initiates HTTP server-related dataflows. */ var httpdi = module.exports = function httpdIConstructor (config) { // we need to launch httpd this.host = config.host; if (!config.port) throw "you must define 'port' key for http initiator"; else this.port = config.port; this.flows = config.workflows || config.dataflows || config.flows; // I don't want to serve static files by default if (config.static) { this.static = config.static === true ? {} : config.static; // - change static root by path this.static.root = project.root.fileIO (this.static.root || 'www'); this.static.index = this.static.index || "index.html"; this.static.headers = this.static.headers || {}; } // - - - prepare configs this.prepare = config.prepare; // if (config.router && process.mainModule.exports[config.router]) { this.router = process.mainModule.exports[config.router]; } else { this.router = this.defaultRouter; } // TODO: use 0.0.0.0 instead of auto // if (this.host == "auto") { // this.detectIP (this.listen); // } else { this.listen (); // } return this; }; util.inherits (httpdi, EventEmitter); httpdi.connections = {}; httpdi.prototype.started = function () { // called from server listen var listenHost = this.host ? this.host : '127.0.0.1'; var listenPort = this.port === 80 ? '' : ':'+this.port; console.log( 'http initiator running at', paint.path ( 'http://'+listenHost+listenPort+'/' ), this.static ? "and serving static files from " + paint.path (project.root.relative (this.static.root)) : "" ); httpdi.connections[this.host+":"+this.port] = this.server; this.ready = true; this.emit ('ready', this.server); }; httpdi.prototype.runPrepare = function (df, request, response) { var self = this; var prepareCfg = df.prepare, prepare = this.prepare; if (prepare) { var dfChain = []; // create chain of wfs var prepareFailure = false; prepareCfg.forEach(function(p, index, arr) { var innerDfConfig = prepare[p]; if (!innerDfConfig || !innerDfConfig.tasks) { console.error (paint.error('request canceled:'), 'no prepare task named "'+p+'"'); // self.emit ('error', 'no prepare task named "'+p+'"'); prepareFailure = true; var presenter = self.createPresenter({}, request, response, 'failed'); // var presenter = self.createPresenter(cDF, 'failed'); if (presenter) presenter.run (); return; } innerDfConfig.idPrefix = df.coloredId + '>'; var innerDf = new flow(innerDfConfig, { request: request, response: response, stage: 'prepare'} ); dfChain.push(innerDf); }); if (prepareFailure) { return; } // push main df to chain dfChain.push(df); // subscribe they for (var i = 0; i < dfChain.length-1; i++) { var currentDf = dfChain[i]; currentDf.nextDf = dfChain[i+1]; currentDf.on('completed', function(cDF) { setTimeout(cDF.nextDf.run.bind (cDF.nextDf), 0); }); currentDf.on('failed', function(cDF) { var presenter = self.createPresenter(cDF, request, response, 'failed'); if (presenter) presenter.run (); }); } dfChain[0].run(); } else { throw "Config doesn't contain such prepare type: " + df.prepare; } }; httpdi.prototype.createPresenter = function (df, request, response, state) { var self = this; // presenter can be: // {completed: ..., failed: ..., failedRequire: ...} — succeeded or failed tasks in dataflow or failed require step // "template.name" — template file for presenter // {"type": "json"} — presenter config // TODO: [{...}, {...}] — presentation dataflow if (!df.presenter) { this.finishRequest (response); return; } // TODO: emit SOMETHING var presenter = df.presenter; //console.log ('running presenter on state: ', state, presenter[state]); // {completed: ..., failed: ..., failedRequire: ...} if (presenter[state]) presenter = presenter[state]; var tasks = []; if (Object.is('String', presenter)) { // "template.name" // WTF? not sure about use case. we want all data declared explicitly. // but here is no data passed to presenter tasks.push ({ file: presenter, //vars: "{$vars}", response: "{$response}", $class: "presenter", $important: true }); } else if (Object.is('Array', presenter)) { // TODO: [{...}, {...}] presenter.map (function (item) { var task = {}; util.extend (true, task, item); task.response = "{$response}"; task.vars = task.vars || task.data || {}; if (!Object.keys (task.vars).length && task.dump) task.vars = df.data; if (!task.functionName || !task.$function) { task.className = task.$class || task.className || "presenter"; task.$important = true; } tasks.push (task); }); } else { // {"type": "json"} presenter.response = "{$response}"; if (!presenter.vars && !presenter.data && presenter.dump) { presenter.vars = {}; var skip = {}; "request|response|global|appMain|project".split ('|').forEach (function (k) { skip[k] = true; }); // WHY IS DF.DATA IS FILLED WITH JUNK??? for (var k in df.data) { if (!skip[k]) { presenter.vars[k] = df.data[k]; } } } else { presenter.vars = presenter.vars || presenter.data || {}; } if (!presenter.functionName || !presenter.$function) { presenter.className = presenter.$class || presenter.className || "presenter"; presenter.$important = true; } tasks.push (presenter); } var reqParams = util.extend(true, { error: df.error, request: request, response: response }, df.data); var presenterDf = new flow ({ id: df.id, tasks: tasks, stage: 'presentation' }, reqParams); presenterDf.on ('completed', function () { //self.log ('presenter done'); self.finishRequest (response); }); presenterDf.on ('failed', function () { presenterDf.log ('Presenter failed: ' + request.method + ' to ' + request.url.pathname); var df500 = self.createFlowByCode(500, request, response); if (df500) { df500.on ('completed', self.finishRequest.bind (self, response)); df500.on ('failed', self.finishRequest.bind (self, response)); } else { self.finishRequest (response); } }); return presenterDf; }; httpdi.prototype.finishRequest = function (res) { if (!res.finished) res.end (); }; httpdi.prototype.createFlow = function (cfg, req, res) { var self = this; if (cfg.static) { return false; } // task MUST contain tasks or presenter if (!cfg.tasks && !cfg.presenter) return; try { var df = new flow( util.extend (true, {}, cfg), { request: req, response: res } ); } catch (e) { console.error ('dataflow failed', req.method, req.url.pathname); throw e; } console.log ('dataflow', req.method, req.url.pathname, df.coloredId); df.on('completed', function (df) { var presenter = self.createPresenter(df, req, res, 'completed'); if (presenter) { presenter.run (); } }); df.on('failed', function (df) { var presenter = self.createPresenter(df, req, res, 'failed'); if (presenter) { presenter.run (); } }); self.emit('detected', req, res, df); if (cfg.prepare) { self.runPrepare(df, req, res); } else { df.run(); } return df; }; httpdi.prototype.createFlowByCode = function (code, req, res) { res.statusCode = code; // find a flow w/ presenter by HTTP response code if (!this.flows._codeFlows) { this.flows._codeFlows = {}; } if (!(res.statusCode in this.flows._codeFlows)) { this.flows._codeFlows[ res.statusCode ] = this.flows.filter(function (df) { return df.code == res.statusCode; })[0]; } var codeDfConfig = this.flows._codeFlows[res.statusCode]; if (codeDfConfig) { if (!codeDfConfig.tasks) { codeDfConfig.tasks = []; } var df = this.createFlow(codeDfConfig, req, res); if (df) { df.on ('completed', this.finishRequest.bind (this, res)); df.on ('failed', this.finishRequest.bind (this, res)); return true; } } this.finishRequest (res); return false; }; httpdi.prototype.initFlow = function (wfConfig, req) { }; // hierarchical router // TODO: igzakt match + pathInfo // TODO: dirInfo, fileName, fileExtension, fullFileName httpdi.prototype.hierarchical = function (req, res) { var pathName = req.url.pathname; // strip trailing slashes if (pathName.length > 1) { pathName = pathName.replace(/\/+$/, ''); } var pathParts = pathName.split(/\/+/).slice(1); var capture = []; this.hierarchical.tree = this; var routeFinder = this.hierarchical.findByPath.bind (this.hierarchical); if (memoize) routeFinder = memoize (routeFinder); var config = routeFinder ( null, pathParts, 0, capture ); if (config) { req.capture = capture; return this.createFlow(config, req, res); } return null; }; httpdi.prototype.hierarchical.walkList = function ( list, pathParts, level, callback ) { var pathLen = pathParts.length; var listLen = list && list.length; outer: for (var i = 0; i < listLen; i += 1) { var tree = list[i]; for (var j = pathLen; j > level; j -= 1) { var pathFragment = pathParts.slice(level, j).join('/'); if (callback(tree, pathFragment, j - 1)) { break outer; } } } }; httpdi.prototype.hierarchical.findByPath = function ( tree, pathParts, level, capture ) { if (!tree) tree = this.tree; var list = tree.workflows || tree.dataflows || tree.flows; var checkedLevel = level; var branch = null; // exact match this.walkList( list, pathParts, level, function (tree, pathFragment, index) { // console.print('PATH', tree.path, 'FRAGMENT', pathFragment); if (tree.path == pathFragment) { checkedLevel = index; branch = tree; return true; } return false; } ); // pattern match !branch && this.walkList( list, pathParts, level, function (tree, pathFragment, index) { // console.print('PATTERN', tree.pattern, 'FRAGMENT', pathFragment); var match = tree.pattern && pathFragment.match(tree.pattern); if (match) { checkedLevel = index; branch = tree; capture.push.apply(capture, match.slice(1)); return true; } return false; } ); if ((branch && branch.static && checkedLevel >= 0) || checkedLevel >= pathParts.length - 1) { return branch; } else { return branch && this.findByPath( branch, pathParts, checkedLevel + 1, capture ); } }; httpdi.prototype.defaultRouter = httpdi.prototype.hierarchical; httpdi.prototype.httpDate = function (date) { date = date || new Date (); var fstr = "%a, %d %b %Y %H:%M:%S UTC"; var utc = 'getUTC'; //utc = utc ? 'getUTC' : 'get'; var shortDayNames = 'Sun Mon Tue Wed Thu Fri Sat'.split (' '); var shortMonNames = 'Jan Feb Mar Apr May Jun Jul Aug Sep Oct Nov Dec'.split (' '); return fstr.replace (/%[YmdHMSab]/g, function (m) { switch (m) { case '%Y': return date[utc + 'FullYear'] (); // no leading zeros required case '%m': m = 1 + date[utc + 'Month'] (); break; case '%d': m = date[utc + 'Date'] (); break; case '%H': m = date[utc + 'Hours'] (); break; case '%M': m = date[utc + 'Minutes'] (); break; case '%S': m = date[utc + 'Seconds'] (); break; case '%a': return shortDayNames[date[utc + 'Day'] ()]; // no leading zeros required case '%b': return shortMonNames[date[utc + 'Month'] ()]; // no leading zeros required default: return m.slice (1); // unknown code, remove % } // add leading zero if required return ('0' + m).slice (-2); }); }; httpdi.prototype.findHandler = function (req, res) { var df = this.router (req, res); if (df) { if (!df.ready) { console.error ("flow not ready and cannot be started"); } return; } // console.log ('httpdi not detected: ' + req.method + ' to ' + req.url.pathname); this.emit ("unknown", req, res); // NOTE: we don't want to serve static files using nodejs. // NOTE: but for rapid development this is acceptable. // NOTE: you MUST write static: false for production if (this.static) { this.handleStatic (req, res); } else { this.createFlowByCode (404, req, res) || res.end(); } }; httpdi.prototype.handleStatic = function (req, res) { var self = this; var isIndex = /\/$/.test(req.url.pathname) ? self.static.index : ''; var pathName = path.join ( self.static.root.path, path.join('.', req.url.pathname), isIndex ); console.log ('filesys ', req.method, req.url.pathname, isIndex ? '=> '+ isIndex : ''); var contentType, charset; // make sure html return fast as possible // if ('.html' == path.extname(pathName)) { // contentType = 'text/html'; // charset = 'utf-8'; // } else if (mime && mime.lookup) { contentType = mime.lookup (pathName); // The logic for charset lookups is pretty rudimentary. if (contentType.match (/^text\//)) charset = mime.charsets.lookup(contentType, 'utf-8'); if (charset) contentType += '; charset='+charset; } else if (!contentType) { console.error( 'sorry, there is no content type for %s', pathName ); } var file = project.root.fileIO(pathName); var fileOptions = {flags: "r"}; var statusCode = 200; var start = 0; var end = 0; var rangeHeader = req.headers.range; if (rangeHeader != null) { // console.log (rangeHeader); var range = rangeHeader.split ('bytes=')[1].split ('-'); start = parseInt(range[0]); end = parseInt(range[1]); if (!isNaN(start)) { if (!isNaN(end) && start > end) { // error, return 200 } else { statusCode = 206; fileOptions.start = start; if (!isNaN(end)) fileOptions.end = end; // console.log ( // 'Browser requested bytes from %d to %d of file %s', // start, end, file.name // ); } } } file.readStream (fileOptions, function (readStream, stats) { if (!stats) { self.createFlowByCode (404, req, res); return; } // if (isNaN(end) || end == 0) end = stat.size-1; if (stats.isDirectory() && !readStream) { res.statusCode = 303; res.setHeader('Location', req.url.pathname +'/'); res.end('Redirecting to ' + req.url.pathname +'/'); return; } else if (stats.isFile() && readStream) { var headers = {}; var uri = req.url.pathname; while (uri.length > 1) { var h = self.static.headers[uri]; headers = util.extend(headers, h || {}); uri = path.dirname(uri); } var headersExtend = { 'Content-Type': contentType, 'Content-Length': stats.size, 'Date': self.httpDate (stats.mtime), }; if (project.config.debug) { headersExtend['Cache-Control'] = 'no-store, no-cache'; } if (statusCode == 206) { end = fileOptions.end ? fileOptions.end : stats.size-1; headersExtend['Content-Range'] = 'bytes '+fileOptions.start+'-'+(end)+'/'+stats.size; headersExtend["Accept-Ranges"] = "bytes"; headersExtend["Content-Length"] = end - fileOptions.start + 1; // console.log (headersExtend); } headers = util.extend (headers, headersExtend); req.on('close', function() { readStream.destroy(); }); res.writeHead (statusCode, headers); readStream.pipe (res); readStream.resume (); return; } self.handleFileStream (stats, readStream, req, res); }); }; httpdi.prototype.listen = function () { var self = this; this.server = http.createServer (function (req, res) { req.pause (); // console.log ('serving: ' + req.method + ' ' + req.url + ' for ', req.connection.remoteAddress + ':' + req.connection.remotePort); // here we need to find matching flows // for received request req.url = url.parse (req.url, true); // use for flow match req[req.method] = true; self.findHandler (req, res); }); var listenArgs = [this.port]; if (this.host) { listenArgs.push (this.host); } listenArgs.push (function () { self.started (); }); this.server.listen.apply (this.server, listenArgs); };