├── .gitignore ├── LICENSE ├── README.md ├── index.js ├── lib ├── agent_manager.js ├── context.js ├── manager.js └── service.js ├── package.json └── tests └── counter-test.js /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Fred Chien(錢逢祥) 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 | # Engined 2 | 3 | A micro framework for application in Node.js. it aims to provide a way to manage your services inside instance. 4 | 5 | ## Requirements 6 | 7 | Engined widely uses async/await in ES6+ so Node.js v7.4+ is required and v7.6 is prefered. If you are working on Node.js v7.6, you have to run Node.js with `--harmony-async-await` options. 8 | 9 | 10 | ## Installation 11 | 12 | You can just install module via NPM: 13 | 14 | ```shell 15 | npm install engined 16 | ``` 17 | 18 | ## Get Started 19 | 20 | Here is an example to use engined to manange services: 21 | 22 | ```javascript 23 | const { Manager, Service } = require('engined'); 24 | 25 | class MyService extends Service { 26 | 27 | constructor(context) { 28 | super(context); 29 | 30 | this.counter = 0; 31 | } 32 | 33 | delay(interval) { 34 | return new Promise((resolve) => { 35 | setTimeout(resolve, interval); 36 | }); 37 | } 38 | 39 | async start() { 40 | 41 | // Getting global counter from context 42 | this.counter = this.getContext().get('global_counter') || 0; 43 | 44 | for (let index = 0; index < 10; index++) { 45 | this.counter++; 46 | console.log(this.counter); 47 | await this.delay(100); 48 | } 49 | 50 | this.getContext().set('global_counter', this.counter); 51 | } 52 | 53 | async stop() { 54 | 55 | // Getting global counter from context 56 | this.counter = this.getContext().get('global_counter') || 0; 57 | 58 | for (let index = 0; index < 10; index++) { 59 | this.counter--; 60 | console.log(this.counter); 61 | await this.delay(100); 62 | } 63 | 64 | this.getContext().set('global_counter', this.counter); 65 | } 66 | } 67 | 68 | const main = async () => { 69 | 70 | // Create manager 71 | let serviceManager = new Manager({ verbose: true }); 72 | 73 | // Adding services to manager 74 | serviceManager.add('MyService1', MyService); 75 | serviceManager.add('MyService2', MyService); 76 | 77 | // Start all services and stop 78 | await serviceManager.startAll(); 79 | await serviceManager.stopAll(); 80 | 81 | console.log('exit'); 82 | }; 83 | 84 | main(); 85 | ``` 86 | 87 | ## Usage 88 | 89 | `engined.Manager` class provides serveral methods for service management. 90 | 91 | ### Load modules at once 92 | 93 | There is a way to load multiple modules with `loadServices` method at once. 94 | 95 | ```javascript 96 | await serviceManager.loadServices({ 97 | MyService1: MyService, 98 | MyService2: MyService 99 | }); 100 | ``` 101 | 102 | ### Add and remove specific service 103 | 104 | Add and remove specific service with `add` and `remove` method. 105 | 106 | ```javascript 107 | serviceManager.add('MyService1', MyService); 108 | serviceManager.remove('MyService1'); 109 | ``` 110 | 111 | Note that `remove` will stop service if service is running. 112 | 113 | ### Start and stop specific service 114 | 115 | It can start and stop specific service with `start` and `stop` method. 116 | 117 | ```javascript 118 | await serviceManager.start('MyService1'); 119 | await serviceManager.stop('MyService1'); 120 | ``` 121 | 122 | ## Accessing Context 123 | 124 | Sharing data among services is allowed, just save it in the context object. `engined.Manager` and `engined.Service` classes provide a method for accessing the context object. 125 | 126 | ### Get context via service manager 127 | 128 | `engined.Manager` provide `getContext` method to get current `Context` instance: 129 | 130 | ```javascript 131 | let context = serviceManager.getContext(); 132 | ``` 133 | 134 | ### Get context from inside the service 135 | 136 | `engined.Service` provide `getContext` method to get current `Context` instance: 137 | 138 | ```javascript 139 | let context = this.getContext(); 140 | ``` 141 | 142 | ### Store data in context object 143 | 144 | Store custom key/value pairs by calling `set` method. 145 | 146 | ```javascript 147 | context.set('mykey', 'test'); 148 | ``` 149 | 150 | ### Get data from context object 151 | 152 | Get key/value pairs by calling `get` method. 153 | 154 | ```javascript 155 | let mykey = context.get('mykey'); 156 | ``` 157 | 158 | ## Agent Manager 159 | 160 | `engined.AgentManager` was designed in order to manage multiple agents. We usually expose agent manager to other services via context object. 161 | 162 | Manager service can be implmeneted like below: 163 | 164 | ```javascript 165 | class fooManagerService extends Service { 166 | 167 | constructor(context) { 168 | super(context); 169 | 170 | this.agentManager = null; 171 | } 172 | 173 | 174 | async start() { 175 | this.agentManager = new AgentManager(); 176 | this.getContext().set('foo', agentManager); 177 | } 178 | 179 | async stop() { 180 | 181 | if (this.agentManager === null) 182 | return; 183 | 184 | this.getContext().remove('foo'); 185 | this.agentManager = null; 186 | 187 | } 188 | 189 | ``` 190 | 191 | Then we can implement agent service and register agent in above manager service: 192 | 193 | ```javascript 194 | class fooAgentService extends Service { 195 | 196 | constructor(context) { 197 | super(context); 198 | 199 | this.agent = null; 200 | } 201 | 202 | 203 | async start() { 204 | this.agent = { 205 | data: 'Hello' 206 | }; 207 | this.getContext().get('foo').register('AgentA', this.agent); 208 | } 209 | 210 | async stop() { 211 | 212 | if (this.agent === null) 213 | return; 214 | 215 | this.getContext().get('foo').unregister('AgentA'); 216 | this.agent = null; 217 | 218 | } 219 | ``` 220 | 221 | In other services, `getAgent()` is the way to accessing specific agent of manager. 222 | 223 | ```javascript 224 | let agent = this.getContext().get('foo').getAgent('AgentA'); 225 | 226 | console.log(agent.data); 227 | ``` 228 | 229 | ### Easy way to assert agent manager in context 230 | 231 | There is a way to create agent manager then register on context faster. 232 | 233 | ```javascript 234 | this.getContext().assert('foo'); 235 | ``` 236 | 237 | ## License 238 | Licensed under the MIT License 239 | 240 | ## Authors 241 | Copyright(c) 2017 Fred Chien(錢逢祥) <> 242 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = { 3 | Context: require('./lib/context'), 4 | Manager: require('./lib/manager'), 5 | Service: require('./lib/service'), 6 | AgentManager: require('./lib/agent_manager') 7 | }; 8 | -------------------------------------------------------------------------------- /lib/agent_manager.js: -------------------------------------------------------------------------------- 1 | module.exports = class AgentManager { 2 | constructor() { 3 | this.agents = {}; 4 | } 5 | 6 | register(agentName, agent) { 7 | if (this.agents[agentName] !== undefined) 8 | throw new Error('Failed to register. ' + agentName + ' agent exists already'); 9 | 10 | this.agents[agentName] = agent; 11 | } 12 | 13 | unregister(agentName) { 14 | 15 | if (this.agents[agentName] !== undefined) 16 | delete this.agents[agentName]; 17 | } 18 | 19 | getAgent(agentName) { 20 | return this.agents[agentName] 21 | } 22 | 23 | count() { 24 | return Object.keys(this.agents).length; 25 | } 26 | }; 27 | -------------------------------------------------------------------------------- /lib/context.js: -------------------------------------------------------------------------------- 1 | const AgentManager = require('./agent_manager'); 2 | 3 | module.exports = class { 4 | 5 | constructor(initialObj) { 6 | this.data = initialObj || {}; 7 | } 8 | 9 | set(key, value) { 10 | this.data[key] = value; 11 | } 12 | 13 | get(key) { 14 | return this.data[key]; 15 | } 16 | 17 | remove(key) { 18 | if (this.data[key] !== undefined) { 19 | delete this.data[key]; 20 | } 21 | } 22 | 23 | assert(key) { 24 | 25 | let agentManager = this.data[key]; 26 | 27 | if (agentManager === undefined) { 28 | agentManager = this.data[key] = new AgentManager(); 29 | } 30 | 31 | return agentManager; 32 | } 33 | }; 34 | -------------------------------------------------------------------------------- /lib/manager.js: -------------------------------------------------------------------------------- 1 | const Context = require('./context'); 2 | 3 | const dummyConsole = { 4 | log: () => {}, 5 | error: () => {}, 6 | warn: () => {} 7 | }; 8 | 9 | module.exports = class { 10 | 11 | constructor() { 12 | 13 | let opts; 14 | if (!(arguments[0] instanceof Context)) { 15 | this.ctx = new Context(); 16 | opts = arguments[0]; 17 | } else { 18 | this.ctx = arguments[0]; 19 | opts = arguments[1]; 20 | } 21 | 22 | this.name = 'Engined'; 23 | this.services = {}; 24 | this.instances = {}; 25 | this.opts = Object.assign({ 26 | verbose: false 27 | }, opts || {}); 28 | 29 | this.console = this.opts.verbose ? console : dummyConsole; 30 | } 31 | 32 | getContext() { 33 | return this.ctx; 34 | } 35 | 36 | async loadServices(services) { 37 | 38 | for (let key in services) { 39 | await this.add(key, services[key]); 40 | } 41 | } 42 | 43 | async add(name, service) { 44 | this.services[name] = service; 45 | } 46 | 47 | async remove(name) { 48 | 49 | await this.stop(name); 50 | 51 | delete this.services[name]; 52 | } 53 | 54 | async start(name) { 55 | 56 | this.console.log('<' + this.name + '>', 'Starting service:', name); 57 | 58 | let service = this.services[name] || null; 59 | if (service === null) 60 | throw new Error('No such service: ' + name); 61 | 62 | let instance = new service(this.ctx); 63 | this.instances[name] = instance; 64 | 65 | // Check dependencies if service have settings 66 | if (instance.dependencies !== undefined) { 67 | 68 | // check whether service exists or not 69 | const inexists = instance.dependencies.find((depName) => { 70 | return (this.services[depName] === undefined); 71 | }); 72 | 73 | if (inexists) { 74 | throw new Error('\"' + name + '\" depends on \"' + inexists + '\", but no such service exists.'); 75 | } 76 | 77 | // check the order of starting 78 | const ret = instance.dependencies.find((depName) => { 79 | return (this.instances[depName] === undefined); 80 | }); 81 | 82 | if (ret) { 83 | throw new Error('\"' + name + '\" depends on \"' + ret + '\", and that service should be started before.'); 84 | } 85 | } 86 | 87 | await instance.start(); 88 | } 89 | 90 | async startAll() { 91 | 92 | this.console.log('<' + this.name + '>', 'Starting all services'); 93 | 94 | for (let key in this.services) { 95 | await this.start(key); 96 | } 97 | 98 | } 99 | 100 | async stop(name) { 101 | 102 | this.console.log('<' + this.name + '>', 'Stopping service:', name); 103 | 104 | let instance = this.instances[name]; 105 | if (!instance) 106 | return; 107 | 108 | await instance.stop(); 109 | delete this.instances[name]; 110 | } 111 | 112 | async stopAll() { 113 | 114 | this.console.log('<' + this.name + '>', 'Stopping all services'); 115 | 116 | // Reverse service ordering 117 | let names = Object.keys(this.instances).reverse()[Symbol.iterator](); 118 | 119 | for (let name of names) { 120 | await this.stop(name); 121 | } 122 | } 123 | }; 124 | -------------------------------------------------------------------------------- /lib/service.js: -------------------------------------------------------------------------------- 1 | const Service = class { 2 | 3 | constructor(ctx) { 4 | this.ctx = ctx; 5 | this.dependencies = []; 6 | this.data = new Map(); 7 | } 8 | 9 | getContext() { 10 | return this.ctx; 11 | } 12 | 13 | getData() { 14 | return this.data; 15 | } 16 | 17 | async start() { 18 | console.log('Not implmeneted:', 'Service.start()'); 19 | } 20 | 21 | async stop() { 22 | console.log('Not implmeneted', 'Service.stop()'); 23 | } 24 | }; 25 | 26 | Service.create = function(data) { 27 | 28 | const userdata = data; 29 | 30 | const customizedService = class extends this { 31 | 32 | constructor(ctx) { 33 | super(ctx); 34 | 35 | Object.entries(userdata).forEach(([ key, value ]) => { 36 | this.data.set(key, value); 37 | }); 38 | } 39 | }; 40 | 41 | customizedService.getData = function() { 42 | return userdata; 43 | }; 44 | 45 | return customizedService; 46 | }; 47 | 48 | module.exports = Service; 49 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "engined", 3 | "version": "0.0.5", 4 | "description": "implementation for managing internal services", 5 | "main": "index.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1" 8 | }, 9 | "repository": { 10 | "type": "git", 11 | "url": "git+https://github.com/cfsghost/engined.git" 12 | }, 13 | "keywords": [ 14 | "service", 15 | "engine" 16 | ], 17 | "engines": { 18 | "node": ">=7.4" 19 | }, 20 | "author": "Fred Chien ", 21 | "license": "MIT", 22 | "bugs": { 23 | "url": "https://github.com/cfsghost/engined/issues" 24 | }, 25 | "homepage": "https://github.com/cfsghost/engined#readme" 26 | } 27 | -------------------------------------------------------------------------------- /tests/counter-test.js: -------------------------------------------------------------------------------- 1 | const { Manager, Service } = require('../'); 2 | 3 | class MyService extends Service { 4 | 5 | constructor(context) { 6 | super(context); 7 | 8 | this.counter = 0; 9 | } 10 | 11 | delay(interval) { 12 | return new Promise((resolve) => { 13 | setTimeout(resolve, interval); 14 | }); 15 | } 16 | 17 | async start() { 18 | 19 | // Getting global counter from context 20 | this.counter = this.getContext().get('global_counter') || 0; 21 | 22 | for (let index = 0; index < 10; index++) { 23 | this.counter++; 24 | console.log(this.counter); 25 | await this.delay(100); 26 | } 27 | 28 | this.getContext().set('global_counter', this.counter); 29 | } 30 | 31 | async stop() { 32 | 33 | // Getting global counter from context 34 | this.counter = this.getContext().get('global_counter') || 0; 35 | 36 | for (let index = 0; index < 10; index++) { 37 | this.counter--; 38 | console.log(this.counter); 39 | await this.delay(100); 40 | } 41 | 42 | this.getContext().set('global_counter', this.counter); 43 | } 44 | } 45 | 46 | const main = async () => { 47 | 48 | // Create manager 49 | let serviceManager = new Manager({ verbose: true }); 50 | 51 | // Adding services to manager 52 | serviceManager.add('MyService1', MyService); 53 | serviceManager.add('MyService2', MyService); 54 | 55 | // Start all services and stop 56 | await serviceManager.startAll(); 57 | await serviceManager.stopAll(); 58 | 59 | console.log('exit'); 60 | }; 61 | 62 | main(); 63 | --------------------------------------------------------------------------------