UNPKG

msgflo

Version:

Polyglot FBP runtime based on message queues

385 lines (356 loc) 11.7 kB
var EventEmitter, Library, baseComponentCommand, cleanComponentDefinition, common, componentCommandForFile, componentsFromConfig, componentsFromDirectory, debug, defaultHandlers, ext, extensionToLanguage, fs, lang, languageExtensions, normalizeConfig, path, replaceMarker, replaceVariables, yaml, indexOf = [].indexOf; fs = require('fs'); path = require('path'); yaml = require('js-yaml'); debug = require('debug')('msgflo:library'); EventEmitter = require('events').EventEmitter; common = require('./common'); defaultHandlers = { ".yml": "msgflo-register --role #ROLE:#FILENAME", ".js": "msgflo-nodejs --name #ROLE #FILENAME", ".coffee": "msgflo-nodejs --name #ROLE #FILENAME", ".py": "msgflo-python #FILENAME #ROLE", ".json": "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS", ".fbp": "noflo-runtime-msgflo --name #ROLE --graph #COMPONENT --iips #IIPS" }; languageExtensions = { 'python': 'py', 'coffeescript': 'coffee', 'javascript': 'js', 'c++': 'cpp', 'rust': 'rs', 'yaml': 'yml' }; extensionToLanguage = {}; for (lang in languageExtensions) { ext = languageExtensions[lang]; extensionToLanguage[`.${ext}`] = lang; } replaceMarker = function(str, marker, value) { marker = '#' + marker.toUpperCase(); return str.replace(new RegExp(marker, 'g'), value); }; exports.replaceVariables = replaceVariables = function(str, variables) { var marker, value; for (marker in variables) { value = variables[marker]; str = replaceMarker(str, marker, value); } return str; }; baseComponentCommand = function(config, component, cmd, filename) { var componentName, variables; variables = common.clone(config.variables); componentName = component.split('/')[1]; if (!componentName) { componentName = component; } if (filename) { variables['FILENAME'] = filename; } variables['COMPONENTNAME'] = componentName; variables['COMPONENT'] = component; return replaceVariables(cmd, variables); }; componentCommandForFile = function(config, filename) { var cmd, component; ext = path.extname(filename); component = path.basename(filename, ext); cmd = config.handlers[ext]; return baseComponentCommand(config, component, cmd, filename); }; componentsFromConfig = function(config) { var cmd, component, components, ref; components = {}; ref = config.components; for (component in ref) { cmd = ref[component]; components[component] = { language: null, // XXX: Could try to guess from cmd/template?? command: baseComponentCommand(config, component, cmd) }; } return components; }; componentsFromDirectory = function(directory, config, callback) { var components, extensions; components = {}; extensions = Object.keys(config.handlers); return fs.exists(directory, function(exists) { if (!exists) { return callback(null, {}); } return fs.readdir(directory, function(err, filenames) { var component, filename, filepath, i, len, supported, unsupported; if (err) { return callback(err); } supported = filenames.filter(function(f) { var ref; return ref = path.extname(f), indexOf.call(extensions, ref) >= 0; }); unsupported = filenames.filter(function(f) { var ref; return !(ref = path.extname(f), indexOf.call(extensions, ref) >= 0); }); if (unsupported.length) { debug('unsupported component files', unsupported); } for (i = 0, len = supported.length; i < len; i++) { filename = supported[i]; ext = path.extname(filename); lang = extensionToLanguage[ext]; component = path.basename(filename, ext); if (config.namespace) { component = `${config.namespace}/${component}`; } debug('loading component from file', filename, component); filepath = path.join(directory, filename); components[component] = { language: lang, command: componentCommandForFile(config, filepath) }; } return callback(null, components); }); }); }; normalizeConfig = function(config) { var k, namespace, ref, repository, v; if (!config) { config = {}; } namespace = config.name || null; repository = config.repository; if ((ref = config.repository) != null ? ref.url : void 0) { // package.json convention repository = config.repository.url; } if (config.msgflo) { // Migth be under a .msgflo key, for instance in package.json config = config.msgflo; } if (typeof config.repository !== 'string') { config.repository = repository; } if (config.namespace == null) { config.namespace = namespace; } if (!config.components) { config.components = {}; } if (!config.variables) { config.variables = {}; } if (!config.handlers) { config.handlers = {}; } for (k in defaultHandlers) { v = defaultHandlers[k]; if (!config.handlers[k]) { config.handlers[k] = defaultHandlers[k]; } } return config; }; // Remove instance-specific data like role and extra from library data cleanComponentDefinition = function(discovered) { var component, i, j, len, len1, port, ref, ref1; if (!(discovered != null ? discovered.definition : void 0)) { return discovered; } // Start by cloning the definition component = common.clone(discovered); if (!(component != null ? component.definition : void 0)) { return component; } delete component.definition.extra; delete component.definition.id; delete component.definition.role; ref = component.definition.inports; for (i = 0, len = ref.length; i < len; i++) { port = ref[i]; delete port.queue; } ref1 = component.definition.outports; for (j = 0, len1 = ref1.length; j < len1; j++) { port = ref1[j]; delete port.queue; } return component; }; Library = class Library extends EventEmitter { constructor(options) { super(); if (options.configfile) { options.config = JSON.parse(fs.readFileSync(options.configfile, 'utf-8')); } if (!options.componentdir) { console.log('WARNING:', 'Default components directory for MsgFlo will change to "components" in next release'); options.componentdir = 'participants'; } options.config = normalizeConfig(options.config); this.options = options; this.components = {}; // "name" -> { command: "", language: ''|null }. lazy-loaded using load() } getComponent(name) { var withNamespace, withoutNamespace; if (this.components[name]) { // Direct match return this.components[name]; } withoutNamespace = path.basename(name); if (this.components[withoutNamespace]) { return this.components[withoutNamespace]; } if (name.indexOf('/' === -1 && this.options.config.namespace)) { withNamespace = this.options.config.namespace + '/' + name; if (this.components[withNamespace]) { return this.components[withNamespace]; } } return null; } _updateComponents(components) { var comp, discovered, existing, k, name, names, v; names = []; for (name in components) { comp = components[name]; if (!comp) { // removed this.components[name] = null; if (names.indexOf(name) === -1) { names.push(name); } continue; } discovered = cleanComponentDefinition(comp); existing = this.getComponent(name); if (!existing) { // added this.components[name] = discovered; if (names.indexOf(name) === -1) { names.push(name); } continue; } if (JSON.stringify(existing.definition) !== JSON.stringify(discovered.definition)) { // updated for (k in discovered) { v = discovered[k]; this.components[name][k] = v; } if (names.indexOf(name) === -1) { names.push(name); } continue; } } // Send components-changed only if something changed if (names.length) { return this.emit('components-changed', names, this.components); } } load(callback) { return componentsFromDirectory(this.options.componentdir, this.options.config, (err, components) => { if (err) { return callback(err); } this._updateComponents(components); this._updateComponents(componentsFromConfig(this.options.config)); return callback(null); }); } // call when MsgFlo discovery message has come in _updateDefinition(name, def) { var changes; if (!def) { // Ignore participants being removed return; } changes = {}; changes[name] = { definition: def }; return this._updateComponents(changes); } getSource(name, callback) { var basename, component, filename, library, ref, ref1, source; debug('requesting component source', name); component = this.getComponent(name); if (!component) { return callback(new Error(`Component not found for ${name}`)); } basename = name; library = null; if (name.indexOf('/') !== -1) { // FBP protocol component:getsource unfortunately bakes in library in this case [library, basename] = name.split('/'); } else if (((ref = this.options.config) != null ? ref.namespace : void 0) != null) { library = (ref1 = this.options.config) != null ? ref1.namespace : void 0; } if (!component.language) { // Component that doesn't come from handlers, send discovery info since source isn't available debug('component without source', name, component.command); source = { name: basename, library: library, code: yaml.safeDump(component.definition || {}), language: 'discovery' }; return callback(null, source); } lang = component.language; ext = languageExtensions[lang]; filename = path.join(this.options.componentdir, `${basename}.${ext}`); return fs.readFile(filename, 'utf-8', function(err, code) { debug('component source file', filename, lang, err); if (err) { return callback(new Error(`Could not find component source for ${name}: ${err.message}`)); } source = { name: basename, library: library, code: code, language: component.language }; return callback(null, source); }); } addComponent(name, language, code, callback) { var filename, ref; debug('adding component', name, language); ext = languageExtensions[language]; ext = ext || language; // default to input lang for open-ended extensibility filename = path.join(this.options.componentdir, `${path.basename(name)}.${ext}`); if (name.indexOf('/') === -1 && ((ref = this.options.config) != null ? ref.namespace : void 0)) { name = `${this.options.config.namespace}/${name}`; } return fs.writeFile(filename, code, (err) => { var changes; if (err) { return callback(err); } changes = {}; changes[name] = { language: language, command: componentCommandForFile(this.options.config, filename) }; this._updateComponents(changes); return callback(null); }); } componentCommand(component, role, iips = {}) { var cmd, ref, vars; cmd = (ref = this.getComponent(component)) != null ? ref.command : void 0; if (!cmd) { throw new Error(`No component ${component} defined for role ${role}`); } vars = { 'ROLE': role, 'IIPS': `'${JSON.stringify(iips)}'` }; cmd = replaceVariables(cmd, vars); return cmd; } }; exports.Library = Library;