├── .gitignore ├── LICENSE.md ├── README.md ├── bin └── moin.js ├── index.js ├── lib ├── Logger.js ├── PromiseEventEmitter.js └── loader.js ├── logo.png └── package.json /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | 3 | # Created by .ignore support plugin (hsz.mobi) 4 | ### Node template 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | 10 | # Runtime data 11 | pids 12 | *.pid 13 | *.seed 14 | 15 | # Directory for instrumented libs generated by jscoverage/JSCover 16 | lib-cov 17 | 18 | # Coverage directory used by tools like istanbul 19 | coverage 20 | 21 | # nyc test coverage 22 | .nyc_output 23 | 24 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 25 | .grunt 26 | 27 | # node-waf configuration 28 | .lock-wscript 29 | 30 | # Compiled binary addons (http://nodejs.org/api/addons.html) 31 | build/Release 32 | 33 | # Dependency directories 34 | node_modules 35 | jspm_packages 36 | 37 | # Optional npm cache directory 38 | .npm 39 | 40 | # Optional REPL history 41 | .node_repl_history 42 | 43 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Torben Hartmann 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # moin 2 | 3 | [![Join the chat at https://gitter.im/moinjs/moin](https://badges.gitter.im/moinjs/moin.svg)](https://gitter.im/moinjs/moin?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) 4 | ![logo](https://raw.githubusercontent.com/moinjs/moin/master/logo.png) 5 | >Moin (pronounced [ˈmɔɪn]) is a German greeting meaning "hello" and in some places "goodbye". 6 | 7 | Moin is an event-driven microservice application server written in behalf of my bachelor thesis at the FH-Kiel. 8 | 9 | # What is it good for 10 | ## Reloading of Code 11 | Imagine you set up a simple express Webserver. When you make changes to a route, you have to restart the whole application in order to see the changes. 12 | Moin watches your service directory and reloads your Module when something was changed. It automatically stops all your self-set timers, so you don't have to. 13 | 14 | ## Advanced Event System 15 | Ever wanted to have more control over the Event filtering? In any normal Node Application you can only listen on event names. 16 | In Moin, you can filter the events on any property. You can even define dynamic checks like `{event:"httpRequest",method:(m)=>m=="get"||m=="post"}`. 17 | 18 | When you emit an event you can register for the return values of the handlers. This is utilized in the example below. 19 | 20 | # Installation 21 | Just install Moin as a global Package. 22 | ```bash 23 | npm install -g moin 24 | ``` 25 | 26 | To ease the generation of configs and the creation of services you can use the yeoman generator: 27 | ```bash 28 | npm install -g yo generator-moin 29 | ``` 30 | 31 | To initialize the current directory for the use of moin just execute: 32 | ```bash 33 | yo moin 34 | ``` 35 | 36 | To create a new service you can use: 37 | 38 | ```bash 39 | yo moin:service 40 | ``` 41 | 42 | # Docs 43 | You can read the docs at [moinjs.github.io](http://moinjs.github.io) 44 | # Sample: a small Webserver 45 | ### webserver/index.js 46 | ```js 47 | let app = require("express")(); 48 | let server = app.listen(3000, ()=> { 49 | console.log("Webserver started") 50 | }); 51 | 52 | //Send out an event for every get Reguest 53 | app.get("*", function (req, res) { 54 | moin.emit("httpRequest", { 55 | method: "get", 56 | path: req.path 57 | }, 30000) 58 | .then(({values,errors,stats})=> { 59 | //See how many Listeners have responded. 60 | switch(values.length){ 61 | case 0: 62 | res.send(404,"Not Found"); 63 | break; 64 | default: 65 | res.send(200,values.join("
")); 66 | } 67 | }) 68 | }); 69 | 70 | moin.registerUnloadHandler(()=>server.close()); 71 | ```` 72 | 73 | ### hello/index.js 74 | ```js 75 | moin.on({event: "httpRequest", method: "get"}, (event)=>`Moin, ${event.path}!`); 76 | ```` 77 | 78 | This example opens an express Webserver and emits an event, when a url is accessed. The hello service registers for these events and returns a response, which is then served to the Browser. 79 | -------------------------------------------------------------------------------- /bin/moin.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | let fs = require("fs"); 3 | let path = require("path"); 4 | let cwd = process.cwd(); 5 | 6 | if (!fs.existsSync(path.join(cwd, "node_modules"))) { 7 | fs.mkdirSync(path.join(cwd, "node_modules")); 8 | } 9 | 10 | require("../index.js")(cwd, process.argv.length == 3 && process.argv[2] == "init"); -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | let Loader = require("./lib/loader"); 2 | let fs = require("fs"); 3 | let path = require("path"); 4 | let PromiseEventEmitter = require("./lib/PromiseEventEmitter"); 5 | let Logger = require("./lib/Logger"); 6 | 7 | 8 | Array.prototype.unique = function () { 9 | let set = new Set(); 10 | return this.filter(elem=> { 11 | if (set.has(elem))return false; 12 | set.add(elem); 13 | return true; 14 | }); 15 | }; 16 | 17 | Object.deepExtend = function (destination, source) { 18 | for (var property in source) { 19 | if (!source.hasOwnProperty(property))continue; 20 | if (source[property] && source[property].constructor && 21 | source[property].constructor === Object) { 22 | destination[property] = destination[property] || {}; 23 | arguments.callee(destination[property], source[property]); 24 | } else { 25 | destination[property] = source[property]; 26 | } 27 | } 28 | return destination; 29 | }; 30 | 31 | let modulePaths = [ 32 | path.join(__dirname, "node_modules"), path.join(path.dirname(module.parent.filename), "node_modules") 33 | ]; 34 | let settings = { 35 | moin: { 36 | modulePaths: [] 37 | }, 38 | logging: { 39 | level: "debug", 40 | disabled: [] 41 | } 42 | }; 43 | 44 | let moin = function (cwd, init) { 45 | let config = {}; 46 | if (fs.existsSync(path.join(cwd, "config.json"))) { 47 | config = require(path.join(cwd, "config.json")); 48 | } 49 | 50 | let confStr = JSON.stringify(config); 51 | 52 | Loader = Loader((conf)=> { 53 | config = Object.deepExtend(conf, config); 54 | }); 55 | 56 | let _modules = []; 57 | config = Object.deepExtend(settings, config); 58 | let running = false; 59 | 60 | Logger = Logger(config.logging); 61 | let log = new Logger("main"); 62 | 63 | 64 | let _api = (function () { 65 | let custom = {}; 66 | let main = new class extends PromiseEventEmitter { 67 | joinPath(...pathSegments) { 68 | return path.join(cwd, ...pathSegments); 69 | } 70 | 71 | load(path) { 72 | return Loader(path); 73 | } 74 | 75 | getMainModule() { 76 | return module.parent; 77 | } 78 | 79 | getLogger(name) { 80 | return new Logger(name); 81 | } 82 | 83 | registerMethod(name, fnc, after = true) { 84 | if (!custom.hasOwnProperty(name))custom[name] = []; 85 | if (after) { 86 | custom[name].push(fnc); 87 | } else { 88 | custom[name].unshift(fnc); 89 | } 90 | } 91 | }; 92 | 93 | process.on('SIGINT', function () { 94 | log.warn("SIGINT received. stopping Moin"); 95 | _api.emit("exit").then(()=> { 96 | process.exit(0); 97 | }); 98 | }); 99 | 100 | let api = new Proxy(main, { 101 | get(target, name) { 102 | if (name in main)return main[name]; 103 | if (!custom.hasOwnProperty(name))return undefined; 104 | return function () { 105 | let lastValue = undefined; 106 | let args = [...arguments]; 107 | let stop = false; 108 | let thisObj = { 109 | getLastValue(){ 110 | return lastValue; 111 | }, 112 | setArguments(...arguments){ 113 | args = arguments; 114 | }, 115 | stopPropagation(){ 116 | stop = true; 117 | }, 118 | getApi(){ 119 | return api; 120 | } 121 | }; 122 | 123 | custom[name].forEach(function (fnc) { 124 | if (stop)return; 125 | lastValue = fnc.apply(thisObj, args); 126 | }); 127 | return lastValue; 128 | }; 129 | } 130 | }); 131 | api.setThisObject(api); 132 | return api; 133 | })(); 134 | 135 | function scanForFolders(folders) { 136 | return Promise.all(folders.map(folder=> { 137 | return new Promise((resolve, reject)=> { 138 | fs.readdir(folder, function (err, sub) { 139 | if (err) { 140 | resolve([]); 141 | } else { 142 | resolve(sub.map(f=>path.join(folder, f))); 143 | } 144 | }) 145 | }) 146 | })).then(result=>result.reduce((arr, elem)=>arr.concat(elem), [])); 147 | } 148 | 149 | return { 150 | addModulePath(path){ 151 | settings.moin.modulePaths.push(path); 152 | return this; 153 | }, 154 | getApi(){ 155 | return _api; 156 | }, 157 | run(){ 158 | if (running)throw "Allready running"; 159 | running = true; 160 | let lookupPaths = settings.moin.modulePaths.concat(modulePaths).map(p=>path.resolve(p)).unique(); 161 | return scanForFolders(lookupPaths) 162 | .then((folders)=>Promise.all(folders.map(folder=>Loader(folder)))) 163 | .then((modules)=>modules.filter(m=>m != null && m.getType() == "module")) 164 | .then(modules=> { 165 | return new Promise((resolve, reject)=> { 166 | let loadOrder = []; 167 | 168 | while (modules.length > 0) { 169 | let resolvable = modules 170 | .filter(mod=>mod.isResolved()) 171 | .filter(mod=>config[mod.getName()].active); 172 | 173 | modules = modules.filter(mod=>!mod.isResolved()); 174 | if (resolvable.length == 0)throw "Cannot fullfill all dependencies"; 175 | loadOrder = loadOrder.concat(resolvable); 176 | modules.forEach(module=> { 177 | resolvable.forEach(res=> { 178 | module.resolveDependency(res); 179 | }) 180 | }) 181 | } 182 | resolve(loadOrder); 183 | }); 184 | }).then(function (modules) { 185 | if (init)return; 186 | _modules = modules; 187 | _modules.forEach(module=>module.load(_api, config[module.getName()])); 188 | log.info(`${_modules.length} modules loaded. beginning startup`) 189 | }).then(function () { 190 | log.startSpinner("Initializing Modules %s"); 191 | }).then(function (spinner) { 192 | return _api.emit("init"); 193 | }).then(function (spinner) { 194 | log.stopSpinner(); 195 | }).then(function () { 196 | log.info("Startup complete"); 197 | if (confStr != JSON.stringify(config)) { 198 | log.info("Change in Modules detected. Saving config.json"); 199 | fs.writeFileSync(path.join(cwd, "config.json"), JSON.stringify(config, null, 2)); 200 | } 201 | }) 202 | .catch(function (e) { 203 | log.error("Error in startup routine:", e); 204 | }); 205 | } 206 | 207 | }; 208 | }; 209 | 210 | module.exports = function (cwd = path.dirname(module.parent.filename), init = false) { 211 | moin = moin(cwd, init); 212 | return moin.run().then(()=>moin.getApi()); 213 | }; -------------------------------------------------------------------------------- /lib/Logger.js: -------------------------------------------------------------------------------- 1 | let Spinner = require('cli-spinner').Spinner; 2 | let colors = require("colors"); 3 | spinner = new Spinner(""); 4 | let minLevel = 0; 5 | let levelMap = [ 6 | "debug", 7 | "info", 8 | "warning", 9 | "error" 10 | ]; 11 | let disabled = []; 12 | 13 | class Logger { 14 | constructor(name) { 15 | this._name = name; 16 | } 17 | 18 | setName(name) { 19 | this._name = name; 20 | } 21 | 22 | newInstance(name) { 23 | return new Logger(name); 24 | } 25 | 26 | log(level, ...args) { 27 | if (disabled.indexOf(this._name) != -1)return; 28 | if (minLevel > levelMap.indexOf(level))return; 29 | 30 | let colorMap = { 31 | "error": "red", 32 | "debug": "grey", 33 | "info": "cyan", 34 | "warning": "yellow" 35 | }; 36 | let color = colorMap.hasOwnProperty(level) ? colorMap[level] : "white"; 37 | let time = new Date() 38 | .toISOString() 39 | .replace(/T/, ' ') 40 | .replace(/\..+/, ''); 41 | 42 | console.log(`[${time}][${level.toUpperCase()}][${this._name}]`[color], ...args); 43 | } 44 | 45 | error(...args) { 46 | this.log("error", ...args); 47 | } 48 | 49 | warn(...args) { 50 | this.log("warning", ...args); 51 | } 52 | 53 | info(...args) { 54 | this.log("info", ...args); 55 | } 56 | 57 | debug(...args) { 58 | this.log("debug", ...args); 59 | } 60 | 61 | startSpinner(title = "%s") { 62 | title.replace("%s", "%s".red); 63 | spinner.setSpinnerTitle(title); 64 | spinner.start(); 65 | } 66 | 67 | setSpinner(title = "%s") { 68 | title.replace("%s", "%s".red); 69 | spinner.setSpinnerTitle(title); 70 | } 71 | 72 | stopSpinner() { 73 | spinner.stop(true); 74 | } 75 | } 76 | function isNumeric(n) { 77 | return !isNaN(parseFloat(n)) && isFinite(n); 78 | } 79 | module.exports = (conf)=> { 80 | if (isNumeric(conf.level)) { 81 | minLevel = Math.max(0, Math.min(parseInt(conf.level), levelMap.length - 1)); 82 | } else { 83 | let index = levelMap.indexOf(conf.level); 84 | if (index != -1)minLevel = index; 85 | } 86 | disabled = conf.disabled; 87 | return Logger; 88 | }; -------------------------------------------------------------------------------- /lib/PromiseEventEmitter.js: -------------------------------------------------------------------------------- 1 | module.exports = class PromiseEventEmitter { 2 | constructor(thisObj) { 3 | this._listener = {}; 4 | this._thisObj = thisObj; 5 | } 6 | 7 | _getListener(name) { 8 | if (!this._listener.hasOwnProperty(name))return []; 9 | let listener = this._listener[name].map(li=>li.fnc); 10 | this._listener[name] = this._listener[name].filter(li=>li.once == false); 11 | return listener; 12 | } 13 | 14 | setThisObject(obj) { 15 | this._thisObj = obj; 16 | } 17 | 18 | emit(event, ...args) { 19 | return this._getListener(event).reduce((prev, cur)=> { 20 | return prev.then((...arg)=> { 21 | return cur.apply(this._thisObj, args); 22 | }); 23 | }, Promise.resolve()); 24 | } 25 | 26 | emitParallel(event, ...args) { 27 | return Promise.all( 28 | this._getListener(event) 29 | .map(fnc=>fnc.apply(this._thisObj, args)) 30 | ); 31 | } 32 | 33 | on(event, fnc) { 34 | if (!this._listener.hasOwnProperty(event))this._listener[event] = []; 35 | this._listener[event].push({ 36 | fnc, 37 | once: false 38 | }); 39 | } 40 | 41 | once(event, fnc) { 42 | if (!this._listener.hasOwnProperty(event))this._listener[event] = []; 43 | this._listener[event].push({ 44 | fnc, 45 | once: true 46 | }); 47 | } 48 | }; -------------------------------------------------------------------------------- /lib/loader.js: -------------------------------------------------------------------------------- 1 | let fs = require("fs"); 2 | let path = require("path"); 3 | 4 | let saveConf = ()=> { 5 | }; 6 | 7 | function loadJSON(path) { 8 | try { 9 | let content = fs.readFileSync(path).toString(); 10 | let object = JSON.parse(content); 11 | return object; 12 | } catch (e) { 13 | return null; 14 | } 15 | } 16 | 17 | class MoinComponent { 18 | constructor(path, settings) { 19 | this._settings = settings; 20 | this._path = path; 21 | this._dependecies = { 22 | module: [], 23 | service: [] 24 | }; 25 | if (settings.moin.hasOwnProperty("moduleDependencies") && Array.isArray(settings.moin.moduleDependencies)) { 26 | this._dependecies.module = settings.moin.moduleDependencies; 27 | } 28 | if (settings.moin.hasOwnProperty("serviceDependencies") && Array.isArray(settings.moin.serviceDependencies)) { 29 | this._dependecies.service = settings.moin.serviceDependencies; 30 | } 31 | let defOptions = {active: true}; 32 | if (settings.moin.hasOwnProperty("settings")) { 33 | defOptions = Object.deepExtend(defOptions, settings.moin.settings); 34 | } 35 | this._config = defOptions; 36 | } 37 | 38 | resolveDependency(component) { 39 | let type = component.getType(); 40 | let module = component.getName(); 41 | if (this._dependecies.hasOwnProperty(type)) { 42 | let position = this._dependecies[type].indexOf(module); 43 | if (position == -1)return; 44 | this._dependecies[type].splice(position, 1); 45 | } 46 | } 47 | 48 | getName() { 49 | return this._settings.name; 50 | } 51 | 52 | getType() { 53 | return this._settings.moin.type; 54 | } 55 | 56 | isResolved() { 57 | return this._dependecies.module.length + this._dependecies.service.length == 0; 58 | } 59 | 60 | getPath() { 61 | return this._path; 62 | } 63 | 64 | getSettings() { 65 | return this._config; 66 | } 67 | } 68 | class MoinModule extends MoinComponent { 69 | constructor(path, settings) { 70 | super(path, settings); 71 | if (settings.moin.hasOwnProperty("serviceDependencies"))throw "Modules can only have Module Dependencies"; 72 | 73 | saveConf({[this.getName()]: this._config}); 74 | } 75 | 76 | load(api, settings) { 77 | require(this._path)(api, settings); 78 | } 79 | 80 | } 81 | 82 | class MoinService extends MoinComponent { 83 | constructor(path, settings) { 84 | super(path, settings); 85 | } 86 | } 87 | 88 | 89 | module.exports = (fnc)=> { 90 | saveConf = fnc; 91 | return function (modulePath) { 92 | modulePath = path.resolve(modulePath); 93 | return new Promise(function (resolve, reject) { 94 | //Check if Path exists and is a Folder 95 | fs.stat(modulePath, (err, stat)=> { 96 | if (err || stat.isFile()) { 97 | //console.log("No folder", modulePath); 98 | resolve(null); 99 | } else { 100 | //Check if there is a package.json 101 | fs.stat(path.join(modulePath, "package.json"), (err, stat)=> { 102 | if (err) { 103 | //console.log("No package.json", modulePath); 104 | resolve(null); 105 | } else { 106 | try { 107 | //Parse package.json and check for 'moin' config field 108 | let data = loadJSON(path.join(modulePath, "package.json")); 109 | if (!data.hasOwnProperty("moin")) { 110 | resolve(null); 111 | } else { 112 | let moin = data.moin; 113 | if (!moin.hasOwnProperty("type") || !(moin.type == "module" || moin.type == "service")) { 114 | //console.error("moin property in package.json without proper type:", modulePath); 115 | resolve(null); 116 | } else { 117 | fs.stat(path.join(modulePath, "index.js"), (err, stat)=> { 118 | if (err) { 119 | console.log("No index.js", modulePath); 120 | resolve(null); 121 | } else { 122 | if (moin.type == "module") { 123 | resolve(new MoinModule(modulePath, data)); 124 | } else { 125 | resolve(new MoinService(modulePath, data)); 126 | } 127 | } 128 | } 129 | ) 130 | } 131 | } 132 | } catch (e) { 133 | console.error("Error while parsing package.json: " + modulePath, `[${e}]`); 134 | resolve(null); 135 | } 136 | 137 | } 138 | }); 139 | } 140 | }) 141 | }); 142 | } 143 | }; -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/moinjs/moin/beee2ff1ce2b64b6aa3c84302823be99677cdbbc/logo.png -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "moin", 3 | "description": "a Simple Microservice Server", 4 | "keywords": [ 5 | "microservice", 6 | "application-server", 7 | "event-driven" 8 | ], 9 | "author": "Torben Hartmann", 10 | "homapage": "http://moinjs.github.io/", 11 | "license": "MIT", 12 | "repository": "moinjs/moin", 13 | "engines": { 14 | "node": ">=6.0" 15 | }, 16 | "bin": { 17 | "moin": "bin/moin.js" 18 | }, 19 | "version": "1.2.0", 20 | "dependencies": { 21 | "cli-spinner": "^0.2.5", 22 | "colors": "^1.1.2", 23 | "moin-event-system": "^1.0.0", 24 | "moin-fs-watcher": "^1.0.0", 25 | "moin-logo": "^1.0.0", 26 | "moin-service-loader": "^1.0.0", 27 | "moin-remote-dispatcher": "^1.0.0", 28 | "moin-service-settings": "^1.0.0", 29 | "object-merge": "^2.5.1" 30 | } 31 | } 32 | --------------------------------------------------------------------------------