UNPKG

stream-flow-control

Version:
437 lines (400 loc) 19 kB
const yaml = require('js-yaml'); const fs = require('fs'); const path = require('path'); const classes = require('../index'); const vm = require('vm'); const {Writable, Readable} = require('stream'); const wrreg = /^writev?|destroy|final$/; const rereg = /^read|destroy$/; const trreg = /^transform|flush|final$/; const horeg = /^hold|release|check$/; const joreg = /^join$/; const rureg = /^rule$/; const fireg = /^identify|match|criteria$/ /** * Mannager is your "toolbelt" class. It's responsible to regiter classes, fetch instances, parse yaml files and building stream chains. * * @typicalname sfc * @extends Writable */ class Manager { constructor() { this.clean(); } /** * Clean namager state * * Useful when you need to start again * * @returns {Manager} 'this' element for chainable purpose. * */ clean() { this._registry = {}; this._paused = false; this._resumeStack = []; return this; } /** * Set manager in paused mode. * * Use it before parsing files like `sfc.pause().parseFiles(...)` * * Useful when you need all streams to be created before piping * * @returns {Manager} 'this' element for chainable purpose. * */ pause() { this._paused = true; return this; } /** * Set inner streams in fowing mode. * * Use it after parsing files like `sfc.pause().parseFiles(...).resume()` * * Useful when you need all streams to be created before piping * * @returns {Manager} 'this' element for chainable purpose. * */ resume() { if(!this._paused) return this; // ensure all streams are built for(let type in this._registry) { for(let name in this._registry[type]) { this.get(type, name); } } // execute all piping this._resumeStack.forEach(fn => fn()); this._paused = false; return this; } /** * Register a stream * @param {string} type type of stream * @param {string} [name] name for this stream * @param {Writable|Readable} elem the stream to register */ set(type, name, elem) { if(!elem) { elem = name; name = elem.options.name; } if(!this._registry[type]) this._registry[type] = {}; if(!this._registry[type][name]) this._registry[type][name] = {elem}; if(!this._registry[type][name].elem) this._registry[type][name].elem = elem; } /** * Retrieve a stream by type and name. If stream has ended, creates a new instance * @param {string} type type of stream * @param {string} name the name of the stream to retrieve * @returns {Readable|Writable|null} returns the indicated stream or null if not found */ get(type, name) { const reg = this._registry[type] && this._registry[type][name]; if(!reg) return null; if(!reg.elem || (reg.elem instanceof Writable && reg.elem._writableState.ended) || (reg.elem instanceof Readable && reg.elem._readableState.ended)) { if(!reg.build) return null; reg.elem = reg.build() } return reg.elem; } /** * Parse a JSON or YAML string and create streams * @param {string} confStr JSON or YAML string. * @param {NodeCallback} cb Callback returning an error if occurred. */ parse(confStr, cb) { let conf; try { conf = JSON.parse(confStr) } catch(e1) { try { conf = yaml.safeLoadAll(confStr); } catch(e2) { return cb("Probably not valid JSON or YAML."+[e1.message, e2.message]); } } this._build(conf, cb); } _build(conf, options, cb) { if(typeof options == 'function') { cb=options; options={}; } try { if(!Array.isArray(conf)) conf = [conf]; conf.forEach(conf => { let _require={}; if(conf._require) { Object.keys(conf._require).forEach((key)=>{ const m = key.match(/^{(.*)}$/) if(m) { const tmp = require(conf._require[key]) m[1].split(/\s*,\s*/).forEach((key)=>{ _require[key] = tmp[key]; }) } else { _require[key]=require(conf._require[key]) } }); delete conf._require; } _require = {...options.require||{}, ..._require}; const context = {..._require, ...classes}; vm.createContext(context); if(options.goal && !conf.__goal__) throw 'Goal chain must have "__goal__" key to get a starting point'; const _buildMethod = (code, params)=>{ let fn = null; if(Array.isArray(code)) code = code.join('\n'); if(typeof code == 'string') fn = vm.compileFunction(code, params||[], {parsingContext: context, contextExtensions: [process, global, {__dirname: require.main.path, __filename: require.main.filename}]}); return fn; } let _customTypes={}; if(conf._customTypes) { Object.keys(conf._customTypes).forEach((key)=>{ if(!conf._customTypes[key].constructor) conf._customTypes[key] = {constructor: conf._customTypes[key]}; _customTypes[key] = _buildMethod(conf._customTypes[key].constructor, ['options']); }); delete conf._customTypes; } _customTypes = {...options.customTypes||{}, ..._customTypes}; const _getDst = (dst) => { if(Array.isArray(dst)) return dst.map(_getDst); if(options.goal && dst == '__resolve__') { let self = options.goal; return self._resolve_writer; } else if (options.goal && dst == '__reject__') { let self = options.goal; return self._reject_writer; } else { return this.get(dst.type, dst.name); } }; Object.keys(conf).forEach(key => { if(key == "__goal__" || key == "_customTypes" || key == "_editorAttrs" || key == "_require") return; const cnf = conf[key]; if(!cnf.type) throw 'Configuration must have a "type" defined in '+key; if(cnf.type == 'sfc' || cnf.type == 'DataWrapper') throw 'Invalid type '+cnf.type+' in '+key; if(cnf.options) { Object.keys(cnf.options).forEach(op => { let option = cnf.options[op]; if(option._type == 'method') { cnf.options[op] = _buildMethod(option.code, option.params); } }); } if(!this._registry[cnf.type]) this._registry[cnf.type] = {}; this._registry[cnf.type][key] = {build: () => { let elem; if(cnf.hasOwnProperty('constructor')) { if(typeof cnf.constructor != 'function') cnf.constructor = _buildMethod(cnf.constructor, ['options']); elem = cnf.constructor({...cnf.options||{}, name: key}); this.set(cnf.type, key, elem); } else { if(!classes[cnf.type]&&!_customTypes[cnf.type]) throw 'Unrecognized type '+cnf.type+' in '+key; if(classes[cnf.type]) { elem = new context[cnf.type]({...cnf.options||{}, name: key}); } else { elem = _customTypes[cnf.type]({...cnf.options||{}, name: key}); } } if(options.goal) { elem.goal = options.goal; options.goal._registerChild(elem); } if(cnf.methods) { const methods = Object.keys(cnf.methods); methods.forEach(method=>{ const def = cnf.methods[method]; switch(cnf.type) { case 'FlowHold': method = method.replace(horeg, str=>'_'+str); break; case 'FlowJoin': method = method.replace(joreg, str=>'_'+str); break; case 'FlowFirst': method = method.replace(fireg, str=>'_'+str); break; case 'Rule': method = method.replace(rureg, str=>'_'+str); break; case 'Duplex': method = method.replace(wrreg, str=>'_'+str).replace(rereg, str=>'_'+str); break; case 'Readable': method = method.replace(rereg, str=>'_'+str); break; case 'Transform': method = method.replace(trreg, str=>'_'+str); break; case 'Writable': method = method.replace(wrreg, str=>'_'+str); break; } if(typeof def.code != 'function') def.code = _buildMethod(def.code, def.params); if(typeof def.code == 'function') { elem[method] = def.code.bind(elem); } }) } if(cnf.on) { const events = Object.keys(cnf.on); events.forEach(event=>{ const def = cnf.on[event]; if(typeof def.code != 'function') def.code = _buildMethod(def.code, def.params); if(typeof def.code == 'function') { elem.on(event, def.code.bind(elem)); } }); } if(cnf.pon) { const events = Object.keys(cnf.pon); events.forEach(event=>{ const def = cnf.pon[event]; if(typeof def.code != 'function') def.code = _buildMethod(def.code, def.params); if(typeof def.code == 'function') { elem.prependListener(event, def.code.bind(elem)); } }); } if(cnf.once) { const events = Object.keys(cnf.once); events.forEach(event=>{ const def = cnf.once[event]; if(typeof def.code != 'function') def.code = _buildMethod(def.code, def.params); if(typeof def.code == 'function') { elem.once(event, def.code.bind(elem)); } }); } if(cnf.ponce) { const events = Object.keys(cnf.ponce); events.forEach(event=>{ const def = cnf.ponce[event]; if(typeof def.code != 'function') def.code = _buildMethod(def.code, def.params); if(typeof def.code == 'function') { elem.prependOnceListener(event, def.code.bind(elem)); } }); } if(cnf.when && elem.when) { let whens = Array.isArray(cnf.when)?cnf.when:[cnf.when] whens.forEach(when => { if(typeof when.cond != 'function') when.cond = _buildMethod(when.cond, ['payload']); if(typeof when.cond == 'function') { const dst = _getDst(when.dst); if(this._paused) { return this._resumeStack.push(() => elem.when(when.cond.bind(elem), dst)); } elem.when(when.cond.bind(elem), dst); } }); } if(cnf.pipe && elem.pipe) { const pipes = Array.isArray(cnf.pipe)?cnf.pipe:[cnf.pipe]; pipes.forEach(pipe => { const dst = _getDst(pipe); if(this._paused) { return this._resumeStack.push(() => elem.pipe(dst)); } elem.pipe(dst); }); } if(cnf.none && elem.none) { let dst = _getDst(cnf.none); if(this._paused) { this._resumeStack.push(() => elem.none(dst)); } else { elem.none(dst); } } if(cnf.resolve && elem.resolve) { let dst = _getDst(cnf.resolve); if(this._paused) { this._resumeStack.push(() => elem.resolve(dst)); } else { elem.resolve(dst); } } if(cnf.reject && elem.reject) { let dst = _getDst(cnf.reject); if(this._paused) { this._resumeStack.push(() => elem.reject(dst)); } else { elem.reject(dst); } } if(cnf.build && elem.build) { elem.build(cnf.build, {require: _require, customTypes: _customTypes}); } if(cnf.chain && elem.chain) { Object.keys(cnf.chain).forEach((event)=>{ let dst = _getDst(cnf.chain[event]); if(this._paused) { this._resumeStack.push(() => elem.chain(event, dst)); } else { elem.chain(event, dst); } }); } return elem; }} }); if(options.goal) { let startElems = _getDst(conf.__goal__); if(!Array.isArray(startElems)) startElems = [startElems]; startElems = startElems.filter(elem => elem); if(!startElems.length) throw "Manager could not resolve any of the following elements: "+conf.__group__.join(', '); options.goal._start.unpipe(); startElems.forEach(dst => options.goal._start.pipe(dst)); } }); } catch(e) { if (cb) return cb(e); throw e; } cb && cb(); } /** * Parse a file or directory and build streams. If no parameter is passed, parseFiles will automatically search for _sfc_ directory, and files that ends with _.sfc_, _.sfc.yaml_, _.sfc.yml_ or _.sfc.json_. * @param {string|array} [filePaths] path of yaml/json file or a directory containing those files. * @returns {Manager} returns 'this' for chainable purposes */ parseFiles(filePaths) { let autoSearch = false; if(!filePaths) { autoSearch = true; filePaths = './'; } if(!Array.isArray(filePaths)) filePaths = [filePaths]; filePaths.forEach(filePath => { try { let stat = fs.statSync(filePath); if(stat.isDirectory()) { let files = fs.readdirSync(filePath); if(files.length) this.parseFiles(files.filter(file => { if(autoSearch && !/^(.+\.)?sfc(\.(yaml|yml|json))?$/.test(file)) { return false; } return true; }).map(file=>path.join(filePath, file))); } else { if(!/\.(yaml|yml|json|sfc\.?)/.test(path.basename(filePath))) return; let data = fs.readFileSync(filePath); this.parse(data, (err) => { if(err) console.log('Cannot parse file '+filePath+'. '+err); }); } } catch(e) { return console.log(e); } }); return this; } } module.exports.Manager = new Manager();