├── .editorconfig ├── .eslintrc ├── .gitignore ├── .npmrc ├── .nvmrc ├── .travis.yml ├── LICENSE.txt ├── README.md ├── documentation.md ├── events.js ├── hydra.png ├── index.js ├── large-logo.png ├── lib ├── cache.js ├── config.js ├── redis-connection.js ├── server-request.js ├── server-response.js ├── umfmessage.js └── utils.js ├── package.json ├── plugin.js ├── plugins.md ├── specs ├── cache.test.js ├── helpers │ └── chai.js ├── index.test.js ├── umfmessage.test.js └── utils.test.js └── tests └── messaging ├── blue-service.js └── red-service.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["mocha"], 3 | "extends": ["eslint:recommended", "google"], 4 | "parserOptions": { 5 | "ecmaVersion": 6, 6 | "ecmaFeatures": { 7 | "jsx": true 8 | }, 9 | "sourceType": "module" 10 | }, 11 | "rules": { 12 | "valid-jsdoc": [ 2, { 13 | "requireReturn": true, 14 | "requireReturnDescription": true, 15 | "requireParamDescription": true, 16 | "prefer": { 17 | "return": "return" 18 | } 19 | }], 20 | "comma-dangle": 0, 21 | "curly": 2, 22 | "semi": [2, "always"], 23 | "no-console": 0, 24 | "no-debugger": 2, 25 | "no-extra-semi": 2, 26 | "no-constant-condition": 2, 27 | "no-alert": 2, 28 | "one-var-declaration-per-line": [2, "always"], 29 | "operator-linebreak": [ 30 | 2, 31 | "after" 32 | ], 33 | "max-len": [ 34 | 2, 35 | 240 36 | ], 37 | "indent": [ 38 | 2, 39 | 2, 40 | { 41 | "SwitchCase": 1 42 | } 43 | ], 44 | "quotes": [ 45 | 2, 46 | "single", 47 | { 48 | "avoidEscape": true 49 | } 50 | ], 51 | "no-multi-str": 2, 52 | "no-mixed-spaces-and-tabs": 2, 53 | "no-trailing-spaces": 2, 54 | "space-unary-ops": [ 55 | 2, 56 | { 57 | "nonwords": false, 58 | "overrides": {} 59 | } 60 | ], 61 | "one-var": [ 62 | 2, 63 | { 64 | "uninitialized": "always", 65 | "initialized": "never" 66 | } 67 | ], 68 | "no-unused-vars": ["error", { "argsIgnorePattern": "^_" }], 69 | "keyword-spacing": [ 70 | 2, 71 | {} 72 | ], 73 | "space-infix-ops": 2, 74 | "space-before-blocks": [ 75 | 2, 76 | "always" 77 | ], 78 | "eol-last": 2, 79 | "space-in-parens": [ 80 | 2, 81 | "never" 82 | ], 83 | "no-multiple-empty-lines": 2, 84 | "no-multi-spaces": 2, 85 | "key-spacing": [ 86 | 2, 87 | { 88 | "beforeColon": false, 89 | "afterColon": true 90 | } 91 | ] 92 | }, 93 | "env": { 94 | "browser": true, 95 | "node": true, 96 | "es6": true, 97 | "mocha": true 98 | }, 99 | "globals": { 100 | "-": 0, 101 | "define": true, 102 | "expect": true, 103 | "it": true, 104 | "require": true 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | *.log 3 | scratch.* 4 | node_modules/ 5 | properties.js 6 | .nyc_output/ 7 | .vscode/ 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v6.2.1 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | 3 | node_js: 4 | - "6.2.1" 5 | 6 | install: 7 | - npm install 8 | 9 | script: 10 | - npm test 11 | 12 | notifications: 13 | email: false 14 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Flywheel Sports Inc., and Contributors 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 | ![](hydra.png) 2 | 3 | [![npm version](https://badge.fury.io/js/hydra.svg)](https://badge.fury.io/js/hydra) NPM downloads [![Build Status](https://travis-ci.org/flywheelsports/hydra.svg?branch=master)](https://travis-ci.org/flywheelsports/hydra) 4 | 5 | Hydra is a NodeJS package which facilitates building distributed applications such as Microservices. 6 | 7 | Hydra offers features such as service discovery, distributed messaging, message load balancing, logging, presence, and health monitoring. It was announced at [EmpireNode 2016](http://www.dev-conferences.com/en/talks/node-microservices-using-hydra-carlos-justiniano/1536). 8 | 9 | Install the latest stable version via `npm install hydra --save` 10 | 11 | [See our quick start guide](https://www.hydramicroservice.com/docs/quick-start/) and [sample projects](https://www.hydramicroservice.com/resources/#resources) 12 | 13 | If you're just getting started with Node Microservices and you have ExpressJS experience you should first look at our [HydraExpress](https://github.com/flywheelsports/hydra-express) project. 14 | 15 | > If you want a lighter-weight Express integration or you're using Hapi, Koa, Sails.js, Restify or Restana then checkout the [Hydra Integration Project](https://www.npmjs.com/package/hydra-integration). 16 | 17 | ### Documentation 18 | 19 | Visit our dedicated documentation site for hydra at: https://www.hydramicroservice.com 20 | 21 | Hydra works great on AWS using Docker containers and Swarm mode, see: https://www.hydramicroservice.com/docs/docker/docker.html 22 | 23 | ### Join us on Slack! 24 | 25 | Are you using or planning on using Hydra on your project? Join us on Slack for more direct support. https://fwsp-hydra.slack.com To join, email cjus34@gmail.com with your desired username and email address (for invite). 26 | 27 | ### Related projects 28 | 29 | There are many projects on NPM which contain the name `hydra`. The following are official projects related to the Hydra - microservice library. 30 | 31 | * [Hydra](https://github.com/flywheelsports/hydra): hydra core project for use with Non-ExpressJS apps 32 | * [Hydra-Express](https://github.com/flywheelsports/hydra-express): hydra for ExpressJS developers 33 | * [Hydra-Integration](https://www.npmjs.com/package/hydra-integration): Integrating third-party Node.js web frameworks with Hydra 34 | * [Hydra-Router](https://github.com/flywheelsports/hydra-router): A service-aware socket and HTTP API router 35 | * [Hydra-cli](https://github.com/flywheelsports/hydra-cli): a hydra commandline client for interacting with Hydra-enabled applications 36 | * [Hydra Generator](https://github.com/flywheelsports/generator-fwsp-hydra): A Yeoman generator for quickly building hydra-based projects 37 | * [Hydra-plugin-rpc](https://www.npmjs.com/package/hydra-plugin-rpc): Create and consume remote procedure calls in hydra with ease 38 | * [Hydra-Cluster](https://github.com/cjus/hydra-cluster): A compute cluster based on Hydra 39 | * [UMF](https://github.com/cjus/umf): Universal Message Format, a messaging specification for routable messages 40 | 41 | ### Examples 42 | 43 | * [A sample hello-service project](https://github.com/cjus/hello-service) 44 | * [Hydra Hot Potato Service - an example of distributed messaging](https://github.com/cjus/hpp-service) 45 | * [Hydra Message Relay - processing WebSocket calls via HydraRouter](https://github.com/cjus/hydra-message-relay) 46 | 47 | ### Articles 48 | 49 | * [Tutorial: Building ExpressJS-based microservices using Hydra](https://community.risingstack.com/tutorial-building-expressjs-based-microservices-using-hydra/) 50 | * [Building a Microservices Example Game with Distributed Messaging](https://community.risingstack.com/building-a-microservices-example-game-with-distributed-messaging/) 51 | * [Deploying Node.js Microservices to AWS using Docker](https://community.risingstack.com/deploying-node-js-microservices-to-aws-using-docker/) 52 | * [Using Docker Swarm for Deploying Node.js Microservices](https://community.risingstack.com/using-docker-swarm-for-deploying-nodejs-microservices/) 53 | 54 | ### Special thanks 55 | 56 | A special thanks to Michael Stillwell for generously transferring his `Hydra` project name on NPM! 57 | -------------------------------------------------------------------------------- /documentation.md: -------------------------------------------------------------------------------- 1 | ![](hydra.png) 2 | 3 | # Hydra documentation 4 | 5 | Our Hydra documentation has been moved to https://www.hydramicroservice.com/ 6 | 7 | # Hydra plugins 8 | 9 | See the [Plugin documentation](/plugins.md). 10 | -------------------------------------------------------------------------------- /events.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @name HydraEvent 5 | * @description EventEmitter event names for Hydra 6 | */ 7 | class HydraEvent { 8 | /** 9 | * @return {string} config update event 10 | * @static 11 | */ 12 | static get CONFIG_UPDATE_EVENT() { 13 | return 'configUpdate'; 14 | } 15 | /** 16 | * @return {string} update message type 17 | * @static 18 | */ 19 | static get UPDATE_MESSAGE_TYPE() { 20 | return 'configRefresh'; 21 | } 22 | } 23 | 24 | module.exports = HydraEvent; 25 | -------------------------------------------------------------------------------- /hydra.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnxtech/hydra/b3a0ac04b1259b9680cbf88b5400a254f6a8fdd9/hydra.png -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('hydra'); 4 | 5 | const Promise = require('bluebird'); 6 | Promise.series = (iterable, action) => { 7 | return Promise.mapSeries( 8 | iterable.map(action), 9 | (value, index, _length) => value || iterable[index].name || null 10 | ); 11 | }; 12 | 13 | const EventEmitter = require('events'); 14 | const util = require('util'); 15 | const uuid = require('uuid'); 16 | const Route = require('route-parser'); 17 | const net = require('net'); 18 | const dns = require('dns'); 19 | const os = require('os'); 20 | const Utils = require('./lib/utils'); 21 | const UMFMessage = require('./lib/umfmessage'); 22 | const RedisConnection = require('./lib/redis-connection'); 23 | const ServerResponse = require('./lib/server-response'); 24 | let serverResponse = new ServerResponse(); 25 | const ServerRequest = require('./lib/server-request'); 26 | let serverRequest = new ServerRequest(); 27 | const Cache = require('./lib/cache'); 28 | 29 | let HYDRA_REDIS_DB = 0; 30 | const redisPreKey = 'hydra:service'; 31 | const mcMessageKey = 'hydra:service:mc'; 32 | const MAX_ENTRIES_IN_HEALTH_LOG = 64; 33 | const ONE_SECOND = 1000; // milliseconds 34 | const ONE_WEEK_IN_SECONDS = 604800; 35 | const PRESENCE_UPDATE_INTERVAL = ONE_SECOND; 36 | const HEALTH_UPDATE_INTERVAL = ONE_SECOND * 5; 37 | const KEY_EXPIRATION_TTL = 3; // three seconds 38 | const KEYS_PER_SCAN = '100'; 39 | const UMF_INVALID_MESSAGE = 'UMF message requires "to", "from" and "body" fields'; 40 | const INSTANCE_ID_NOT_SET = 'not set'; 41 | 42 | /** 43 | * @name Hydra 44 | * @summary Base class for Hydra. 45 | * @fires Hydra#log 46 | * @fires Hydra#message 47 | */ 48 | class Hydra extends EventEmitter { 49 | /** 50 | * @name constructor 51 | * @return {undefined} 52 | */ 53 | constructor() { 54 | super(); 55 | 56 | this.instanceID = INSTANCE_ID_NOT_SET; 57 | this.mcMessageChannelClient; 58 | this.mcDirectMessageChannelClient; 59 | this.publishChannel = null; 60 | this.config = null; 61 | this.serviceName = ''; 62 | this.serviceDescription = ''; 63 | this.serviceVersion = ''; 64 | this.isService = false; 65 | this.redisdb = null; 66 | this._updatePresence = this._updatePresence.bind(this); 67 | this._updateHealthCheck = this._updateHealthCheck.bind(this); 68 | this.registeredRoutes = []; 69 | this.registeredPlugins = []; 70 | this.presenceTimerInteval = null; 71 | this.healthTimerInterval = null; 72 | this.initialized = false; 73 | this.hostName = os.hostname(); 74 | this.internalCache = new Cache(); 75 | this.ready = () => Promise.reject(new Error('You must call hydra.init() before invoking hydra.ready()')); 76 | } 77 | 78 | /** 79 | * @name use 80 | * @summary Adds plugins to Hydra 81 | * @param {...object} plugins - plugins to register 82 | * @return {object} - Promise which will resolve when all plugins are registered 83 | */ 84 | use(...plugins) { 85 | return Promise.series(plugins, (plugin) => this._registerPlugin(plugin)); 86 | } 87 | 88 | /** 89 | * @name _registerPlugin 90 | * @summary Registers a plugin with Hydra 91 | * @param {object} plugin - HydraPlugin to use 92 | * @return {object} Promise or value 93 | */ 94 | _registerPlugin(plugin) { 95 | this.registeredPlugins.push(plugin); 96 | return plugin.setHydra(this); 97 | } 98 | 99 | /** 100 | * @name init 101 | * @summary Register plugins then continue initialization 102 | * @param {mixed} config - a string with a path to a configuration file or an 103 | * object containing hydra specific keys/values 104 | * @param {boolean} testMode - whether hydra is being started in unit test mode 105 | * @return {object} promise - resolves with this._init or rejects with an appropriate 106 | * error if something went wrong 107 | */ 108 | init(config, testMode) { 109 | // Reject() if we've already been called successfully 110 | if (INSTANCE_ID_NOT_SET !== this.instanceID) { 111 | return Promise.reject(new Error('Hydra.init() already invoked')); 112 | } 113 | 114 | this.testMode = testMode; 115 | 116 | if (typeof config === 'string') { 117 | const configHelper = require('./lib/config'); 118 | return configHelper.init(config) 119 | .then(() => { 120 | return this.init(configHelper.getObject(), testMode); 121 | }); 122 | } 123 | 124 | const initPromise = new Promise((resolve, reject) => { 125 | let loader = (newConfig) => { 126 | return Promise.series(this.registeredPlugins, (plugin) => plugin.setConfig(newConfig.hydra)) 127 | .then((..._results) => { 128 | return this._init(newConfig.hydra); 129 | }) 130 | .then(() => { 131 | resolve(newConfig); 132 | return 0; 133 | }) 134 | .catch((err) => { 135 | this._logMessage('error', err.toString()); 136 | reject(err); 137 | }); 138 | }; 139 | 140 | if (!config || !config.hydra) { 141 | config = Object.assign({ 142 | 'hydra': { 143 | 'serviceIP': '', 144 | 'servicePort': 0, 145 | 'serviceType': '', 146 | 'serviceDescription': '', 147 | 'redis': { 148 | 'url': 'redis://127.0.0.1:6379/15' 149 | } 150 | } 151 | }); 152 | } 153 | 154 | if (!config.hydra.redis) { 155 | config.hydra = Object.assign(config.hydra, { 156 | 'redis': { 157 | 'url': 'redis://127.0.0.1:6379/15' 158 | } 159 | }); 160 | } 161 | 162 | if (process.env.HYDRA_REDIS_URL) { 163 | Object.assign(config.hydra, { 164 | redis: { 165 | url: process.env.HYDRA_REDIS_URL 166 | } 167 | }); 168 | } 169 | 170 | let partialConfig = true; 171 | if (process.env.HYDRA_SERVICE) { 172 | let hydraService = process.env.HYDRA_SERVICE.trim(); 173 | if (hydraService[0] === '{') { 174 | let newHydraBranch = Utils.safeJSONParse(hydraService); 175 | Object.assign(config.hydra, newHydraBranch); 176 | partialConfig = false; 177 | } 178 | 179 | if (hydraService.includes('|')) { 180 | hydraService = hydraService.replace(/(\r\n|\r|\n)/g, ''); 181 | let newHydraBranch = {}; 182 | let key = ''; 183 | let val = ''; 184 | let segs = hydraService.split('|'); 185 | segs.forEach((segment) => { 186 | segment = segment.trim(); 187 | [key, val] = segment.split('='); 188 | newHydraBranch[key] = val; 189 | }); 190 | Object.assign(config.hydra, newHydraBranch); 191 | partialConfig = false; 192 | } 193 | } 194 | 195 | if (!config.hydra.serviceName || (!config.hydra.servicePort && !config.hydra.servicePort === 0)) { 196 | reject(new Error('Config missing serviceName or servicePort')); 197 | return; 198 | } 199 | if (config.hydra.serviceName.includes(':')) { 200 | reject(new Error('serviceName can not have a colon character in its name')); 201 | return; 202 | } 203 | if (config.hydra.serviceName.includes(' ')) { 204 | reject(new Error('serviceName can not have a space character in its name')); 205 | return; 206 | } 207 | 208 | if (partialConfig && process.env.HYDRA_REDIS_URL && process.env.HYDRA_SERVICE) { 209 | this._connectToRedis({redis: {url: process.env.HYDRA_REDIS_URL}}) 210 | .then(() => { 211 | if (!this.redisdb) { 212 | reject(new Error('No Redis connection')); 213 | return; 214 | } 215 | this.redisdb.select(HYDRA_REDIS_DB, (err, _result) => { 216 | if (!err) { 217 | this._getConfig(process.env.HYDRA_SERVICE) 218 | .then((storedConfig) => { 219 | this.redisdb.quit(); 220 | if (!storedConfig) { 221 | reject(new Error('Invalid service stored config')); 222 | } else { 223 | return loader(storedConfig); 224 | } 225 | }) 226 | .catch((err) => reject(err)); 227 | } else { 228 | reject(new Error('Invalid service stored config')); 229 | } 230 | }); 231 | }); 232 | } else { 233 | return loader(config); 234 | } 235 | }); 236 | this.ready = () => initPromise; 237 | return initPromise; 238 | } 239 | 240 | /** 241 | * @name _init 242 | * @summary Initialize Hydra with config object. 243 | * @param {object} config - configuration object containing hydra specific keys/values 244 | * @return {object} promise - resolving if init success or rejecting otherwise 245 | */ 246 | _init(config) { 247 | return new Promise((resolve, reject) => { 248 | let ready = () => { 249 | Promise.series(this.registeredPlugins, (plugin) => plugin.onServiceReady()).then((..._results) => { 250 | resolve(); 251 | }).catch((err) => { 252 | this._logMessage('error', err.toString()); 253 | reject(err); 254 | }); 255 | }; 256 | this.config = config; 257 | this._connectToRedis(this.config).then(() => { 258 | if (!this.redisdb) { 259 | reject(new Error('No Redis connection')); 260 | return; 261 | } 262 | 263 | let p = this._parseServicePortConfig(this.config.servicePort); 264 | p.then((port) => { 265 | this.config.servicePort = port; 266 | this.serviceName = config.serviceName; 267 | if (this.serviceName && this.serviceName.length > 0) { 268 | this.serviceName = this.serviceName.toLowerCase(); 269 | } 270 | this.serviceDescription = this.config.serviceDescription || 'not specified'; 271 | this.config.serviceVersion = this.serviceVersion = this.config.serviceVersion || this._getParentPackageJSONVersion(); 272 | 273 | /** 274 | * Check if serviceIP has a wildcard character. 275 | * If so, use the wildcard to mean the IP address pattern up to the wildcard. 276 | * Use the pattern to search for an IP address that matches the pattern. 277 | * If found, use the first matchin IP address as the serviceIP. 278 | * This is useful in docker containers with multiple network interfaces and now way of choosing which one to use. 279 | */ 280 | if (this.config.serviceIP !== '' && this.config.serviceIP.indexOf('*') > -1) { 281 | let starPoint = this.config.serviceIP.indexOf('*'); 282 | let pattern = this.config.serviceIP.substring(0, starPoint); 283 | let firstSelected = false; 284 | let interfaces = os.networkInterfaces(); 285 | Object.keys(interfaces). 286 | forEach((itf) => { 287 | interfaces[itf].forEach((interfaceRecord)=>{ 288 | if (!firstSelected && interfaceRecord.family === 'IPv4' && interfaceRecord.address.indexOf(pattern) === 0) { 289 | this.config.serviceIP = interfaceRecord.address; 290 | firstSelected = true; 291 | } 292 | }); 293 | }); 294 | this._updateInstanceData(); 295 | ready(); 296 | } 297 | /** 298 | * Determine network DNS/IP for this service. 299 | * - First check whether serviceDNS is defined. If so, this is expected to be a DNS entry. 300 | * - Else check whether serviceIP exists and is not empty ('') and is not an segemented IP 301 | * such as 192.168.100.106 If so, then use DNS lookup to determine an actual dotted IP address. 302 | * - Else check whether serviceIP exists and *IS* set to '' - that means the service author is 303 | * asking Hydra to determine the machine's IP address. 304 | * - And final else - the serviceIP is expected to be populated with an actual dotted IP address 305 | * or serviceDNS contains a valid DNS entry. 306 | */ 307 | else if (this.config.serviceDNS && this.config.serviceDNS !== '') { 308 | this.config.serviceIP = this.config.serviceDNS; 309 | this._updateInstanceData(); 310 | ready(); 311 | } else { 312 | if (this.config.serviceIP && this.config.serviceIP !== '' && net.isIP(this.config.serviceIP) === 0) { 313 | dns.lookup(this.config.serviceIP, (err, result) => { 314 | this.config.serviceIP = result; 315 | this._updateInstanceData(); 316 | ready(); 317 | }); 318 | } else if (!this.config.serviceIP || this.config.serviceIP === '') { 319 | // handle IP selection 320 | let interfaces = os.networkInterfaces(); 321 | if (this.config.serviceInterface && this.config.serviceInterface !== '') { 322 | let segments = this.config.serviceInterface.split('/'); 323 | if (segments && segments.length === 2) { 324 | let interfaceName = segments[0]; 325 | let interfaceMask = segments[1]; 326 | Object.keys(interfaces). 327 | forEach((itf) => { 328 | interfaces[itf].forEach((interfaceRecord)=>{ 329 | if (itf === interfaceName && interfaceRecord.netmask === interfaceMask && interfaceRecord.family === 'IPv4') { 330 | this.config.serviceIP = interfaceRecord.address; 331 | } 332 | }); 333 | }); 334 | } else { 335 | throw new Error('config serviceInterface is not a valid format'); 336 | } 337 | } else { 338 | // not using serviceInterface - just select first eth0 entry. 339 | let firstSelected = false; 340 | Object.keys(interfaces). 341 | forEach((itf) => { 342 | interfaces[itf].forEach((interfaceRecord)=>{ 343 | if (!firstSelected && interfaceRecord.family === 'IPv4' && interfaceRecord.address !== '127.0.0.1') { 344 | this.config.serviceIP = interfaceRecord.address; 345 | firstSelected = true; 346 | } 347 | }); 348 | }); 349 | } 350 | this._updateInstanceData(); 351 | ready(); 352 | } else { 353 | this._updateInstanceData(); 354 | ready(); 355 | } 356 | } 357 | return 0; 358 | }).catch((err) => reject(err)); 359 | return p; 360 | }).catch((err) => reject(err)); 361 | }); 362 | } 363 | 364 | /** 365 | * @name _updateInstanceData 366 | * @summary Update instance id and direct message key 367 | * @return {undefined} 368 | */ 369 | _updateInstanceData() { 370 | this.instanceID = this._serverInstanceID(); 371 | this.initialized = true; 372 | } 373 | 374 | /** 375 | * @name _shutdown 376 | * @summary Shutdown hydra safely. 377 | * @return {undefined} 378 | */ 379 | _shutdown() { 380 | return new Promise((resolve) => { 381 | clearInterval(this.presenceTimerInteval); 382 | clearInterval(this.healthTimerInterval); 383 | 384 | const promises = []; 385 | if (!this.testMode) { 386 | this._logMessage('error', 'Service is shutting down.'); 387 | this.redisdb.batch() 388 | .expire(`${redisPreKey}:${this.serviceName}:${this.instanceID}:health`, KEY_EXPIRATION_TTL) 389 | .expire(`${redisPreKey}:${this.serviceName}:${this.instanceID}:health:log`, ONE_WEEK_IN_SECONDS) 390 | .exec(); 391 | 392 | if (this.mcMessageChannelClient) { 393 | promises.push(this.mcMessageChannelClient.quitAsync()); 394 | } 395 | if (this.mcDirectMessageChannelClient) { 396 | promises.push(this.mcDirectMessageChannelClient.quitAsync()); 397 | } 398 | if (this.publishChannel) { 399 | promises.push(this.publishChannel.quitAsync()); 400 | } 401 | } 402 | if (this.redisdb) { 403 | this.redisdb.del(`${redisPreKey}:${this.serviceName}:${this.instanceID}:presence`, () => { 404 | this.redisdb.quit(); 405 | Promise.all(promises).then(resolve); 406 | }); 407 | this.redisdb.quit(); 408 | Promise.all(promises).then(resolve); 409 | } else { 410 | Promise.all(promises).then(resolve); 411 | } 412 | this.initialized = false; 413 | this.instanceID = INSTANCE_ID_NOT_SET; 414 | }); 415 | } 416 | 417 | /** 418 | * @name _connectToRedis 419 | * @summary Configure access to Redis and monitor emitted events. 420 | * @private 421 | * @param {object} config - Redis client configuration 422 | * @return {object} promise - resolves or reject 423 | */ 424 | _connectToRedis(config) { 425 | let retryStrategy = config.redis.retry_strategy; 426 | delete config.redis.retry_strategy; 427 | let redisConnection = new RedisConnection(config.redis, 0, this.testMode); 428 | HYDRA_REDIS_DB = redisConnection.redisConfig.db; 429 | return redisConnection.connect(retryStrategy) 430 | .then((client) => { 431 | this.redisdb = client; 432 | client 433 | .on('reconnecting', () => { 434 | this._logMessage('error', 'Reconnecting to Redis server...'); 435 | }) 436 | .on('warning', (warning) => { 437 | this._logMessage('error', `Redis warning: ${warning}`); 438 | }) 439 | .on('end', () => { 440 | this._logMessage('error', 'Established Redis server connection has closed'); 441 | }) 442 | .on('error', (err) => { 443 | this._logMessage('error', `Redis error: ${err}`); 444 | }); 445 | return client; 446 | }); 447 | } 448 | 449 | /** 450 | * @name _getKeys 451 | * @summary Retrieves a list of Redis keys based on pattern. 452 | * @param {string} pattern - pattern to filter with 453 | * @return {object} promise - promise resolving to array of keys or or empty array 454 | */ 455 | _getKeys(pattern) { 456 | return new Promise((resolve, _reject) => { 457 | if (this.testMode) { 458 | this.redisdb.keys(pattern, (err, result) => { 459 | if (err) { 460 | resolve([]); 461 | } else { 462 | resolve(result); 463 | } 464 | }); 465 | } else { 466 | let doScan = (cursor, pattern, retSet) => { 467 | this.redisdb.scan(cursor, 'MATCH', pattern, 'COUNT', KEYS_PER_SCAN, (err, result) => { 468 | if (!err) { 469 | cursor = result[0]; 470 | let keys = result[1]; 471 | keys.forEach((key, _i) => { 472 | retSet.add(key); 473 | }); 474 | if (cursor === '0') { 475 | resolve(Array.from(retSet)); 476 | } else { 477 | doScan(cursor, pattern, retSet); 478 | } 479 | } else { 480 | resolve([]); 481 | } 482 | }); 483 | }; 484 | let results = new Set(); 485 | doScan('0', pattern, results); 486 | } 487 | }); 488 | } 489 | 490 | /** 491 | * @name _getServiceName 492 | * @summary Retrieves the service name of the current instance. 493 | * @private 494 | * @throws Throws an error if this machine isn't an instance. 495 | * @return {string} serviceName - returns the service name. 496 | */ 497 | _getServiceName() { 498 | if (!this.initialized) { 499 | let msg = 'init() not called, Hydra requires a configuration object.'; 500 | this._logMessage('error', msg); 501 | throw new Error(msg); 502 | } 503 | return this.serviceName; 504 | } 505 | 506 | /** 507 | * @name _serverInstanceID 508 | * @summary Returns the server instance ID. 509 | * @private 510 | * @return {string} instance id 511 | */ 512 | _serverInstanceID() { 513 | return uuid. 514 | v4(). 515 | replace(RegExp('-', 'g'), ''); 516 | } 517 | 518 | /** 519 | * @name _registerService 520 | * @summary Registers this machine as a Hydra instance. 521 | * @description This is an optional call as this module might just be used to monitor and query instances. 522 | * @private 523 | * @return {object} promise - resolving if registration success or rejecting otherwise 524 | */ 525 | _registerService() { 526 | return new Promise((resolve, reject) => { 527 | if (!this.initialized) { 528 | let msg = 'init() not called, Hydra requires a configuration object.'; 529 | this._logMessage('error', msg); 530 | reject(new Error(msg)); 531 | return; 532 | } 533 | 534 | if (!this.redisdb) { 535 | let msg = 'No Redis connection'; 536 | this._logMessage('error', msg); 537 | reject(new Error(msg)); 538 | return; 539 | } 540 | this.isService = true; 541 | let serviceName = this.serviceName; 542 | 543 | let serviceEntry = Utils.safeJSONStringify({ 544 | serviceName, 545 | type: this.config.serviceType, 546 | registeredOn: this._getTimeStamp() 547 | }); 548 | this.redisdb.set(`${redisPreKey}:${serviceName}:service`, serviceEntry, (err, _result) => { 549 | if (err) { 550 | let msg = 'Unable to set :service key in Redis db.'; 551 | this._logMessage('error', msg); 552 | reject(new Error(msg)); 553 | } else { 554 | let testRedis; 555 | if (this.testMode) { 556 | let redisConnection; 557 | redisConnection = new RedisConnection(this.config.redis, 0, this.testMode); 558 | testRedis = redisConnection.getRedis(); 559 | } 560 | // Setup service message courier channels 561 | this.mcMessageChannelClient = this.testMode ? testRedis.createClient() : this.redisdb.duplicate(); 562 | this.mcMessageChannelClient.subscribe(`${mcMessageKey}:${serviceName}`); 563 | this.mcMessageChannelClient.on('message', (channel, message) => { 564 | let msg = Utils.safeJSONParse(message); 565 | if (msg) { 566 | let umfMsg = UMFMessage.createMessage(msg); 567 | this.emit('message', umfMsg.toShort()); 568 | } 569 | }); 570 | 571 | this.mcDirectMessageChannelClient = this.testMode ? testRedis.createClient() : this.redisdb.duplicate(); 572 | this.mcDirectMessageChannelClient.subscribe(`${mcMessageKey}:${serviceName}:${this.instanceID}`); 573 | this.mcDirectMessageChannelClient.on('message', (channel, message) => { 574 | let msg = Utils.safeJSONParse(message); 575 | if (msg) { 576 | let umfMsg = UMFMessage.createMessage(msg); 577 | this.emit('message', umfMsg.toShort()); 578 | } 579 | }); 580 | 581 | // Schedule periodic updates 582 | this.presenceTimerInteval = setInterval(this._updatePresence, PRESENCE_UPDATE_INTERVAL); 583 | this.healthTimerInterval = setInterval(this._updateHealthCheck, HEALTH_UPDATE_INTERVAL); 584 | 585 | // Update presence immediately without waiting for next update interval. 586 | this._updatePresence(); 587 | 588 | resolve({ 589 | serviceName: this.serviceName, 590 | serviceIP: this.config.serviceIP, 591 | servicePort: this.config.servicePort 592 | }); 593 | } 594 | }); 595 | }); 596 | } 597 | 598 | /** 599 | * @name _registerRoutes 600 | * @summary Register routes 601 | * @description Routes must be formatted as UMF To routes. https://github.com/cjus/umf#%20To%20field%20(routing) 602 | * @private 603 | * @param {array} routes - array of routes 604 | * @return {object} Promise - resolving or rejecting 605 | */ 606 | _registerRoutes(routes) { 607 | return new Promise((resolve, reject) => { 608 | if (!this.redisdb) { 609 | reject(new Error('No Redis connection')); 610 | return; 611 | } 612 | this._flushRoutes().then(() => { 613 | let routesKey = `${redisPreKey}:${this.serviceName}:service:routes`; 614 | let trans = this.redisdb.multi(); 615 | [ 616 | `[get]/${this.serviceName}`, 617 | `[get]/${this.serviceName}/`, 618 | `[get]/${this.serviceName}/:rest` 619 | ].forEach((pattern) => { 620 | routes.push(pattern); 621 | }); 622 | routes.forEach((route) => { 623 | trans.sadd(routesKey, route); 624 | }); 625 | trans.exec((err, _result) => { 626 | if (err) { 627 | reject(err); 628 | } else { 629 | return this._getRoutes() 630 | .then((routeList) => { 631 | if (routeList.length) { 632 | this.registeredRoutes = []; 633 | routeList.forEach((route) => { 634 | this.registeredRoutes.push(new Route(route)); 635 | }); 636 | if (this.serviceName !== 'hydra-router') { 637 | // let routers know that a new service route was registered 638 | resolve(); 639 | return this._sendBroadcastMessage(UMFMessage.createMessage({ 640 | to: 'hydra-router:/refresh', 641 | from: `${this.serviceName}:/`, 642 | body: { 643 | action: 'refresh', 644 | serviceName: this.serviceName 645 | } 646 | })); 647 | } else { 648 | resolve(); 649 | } 650 | } else { 651 | resolve(); 652 | } 653 | }) 654 | .catch(reject); 655 | } 656 | }); 657 | }).catch(reject); 658 | }); 659 | } 660 | 661 | /** 662 | * @name _getRoutes 663 | * @summary Retrieves a array list of routes 664 | * @param {string} serviceName - name of service to retrieve list of routes. 665 | * If param is undefined, then the current serviceName is used. 666 | * @return {object} Promise - resolving to array of routes or rejection 667 | */ 668 | _getRoutes(serviceName) { 669 | if (serviceName === undefined) { 670 | serviceName = this.serviceName; 671 | } 672 | return new Promise((resolve, reject) => { 673 | let routesKey = `${redisPreKey}:${serviceName}:service:routes`; 674 | this.redisdb.smembers(routesKey, (err, result) => { 675 | if (err) { 676 | reject(err); 677 | } else { 678 | resolve(result); 679 | } 680 | }); 681 | }); 682 | } 683 | 684 | /** 685 | * @name _getAllServiceRoutes 686 | * @summary Retrieve all service routes. 687 | * @return {object} Promise - resolving to an object with keys and arrays of routes 688 | */ 689 | _getAllServiceRoutes() { 690 | return new Promise((resolve, reject) => { 691 | if (!this.redisdb) { 692 | let msg = 'No Redis connection'; 693 | this._logMessage('error', msg); 694 | reject(new Error(msg)); 695 | return; 696 | } 697 | let promises = []; 698 | let serviceNames = []; 699 | this._getKeys('*:routes') 700 | .then((serviceRoutes) => { 701 | serviceRoutes.forEach((service) => { 702 | let segments = service.split(':'); 703 | let serviceName = segments[2]; 704 | serviceNames.push(serviceName); 705 | promises.push(this._getRoutes(serviceName)); 706 | }); 707 | return Promise.all(promises); 708 | }) 709 | .then((routes) => { 710 | let resObj = {}; 711 | let idx = 0; 712 | routes.forEach((routesList) => { 713 | resObj[serviceNames[idx]] = routesList; 714 | idx += 1; 715 | }); 716 | resolve(resObj); 717 | }) 718 | .catch((err) => { 719 | reject(err); 720 | }); 721 | }); 722 | } 723 | 724 | /** 725 | * @name _matchRoute 726 | * @summary Matches a route path to a list of registered routes 727 | * @private 728 | * @param {string} routePath - a URL path to match 729 | * @return {boolean} match - true if match, false if not 730 | */ 731 | _matchRoute(routePath) { 732 | let ret = false; 733 | for (let route of this.registeredRoutes) { 734 | if (route.match(routePath)) { 735 | ret = true; 736 | break; 737 | } 738 | } 739 | return ret; 740 | } 741 | 742 | /** 743 | * @name _flushRoutes 744 | * @summary Delete's the services routes. 745 | * @return {object} Promise - resolving or rejection 746 | */ 747 | _flushRoutes() { 748 | return new Promise((resolve, reject) => { 749 | let routesKey = `${redisPreKey}:${this.serviceName}:service:routes`; 750 | this.redisdb.del(routesKey, (err, result) => { 751 | if (err) { 752 | reject(err); 753 | } else { 754 | resolve(result); 755 | } 756 | }); 757 | }); 758 | } 759 | 760 | /** 761 | * @name _updatePresence 762 | * @summary Update service presence. 763 | * @private 764 | * @return {undefined} 765 | */ 766 | _updatePresence() { 767 | let entry = Utils.safeJSONStringify({ 768 | serviceName: this.serviceName, 769 | serviceDescription: this.serviceDescription, 770 | version: this.serviceVersion, 771 | instanceID: this.instanceID, 772 | updatedOn: this._getTimeStamp(), 773 | processID: process.pid, 774 | ip: this.config.serviceIP, 775 | port: this.config.servicePort, 776 | hostName: this.hostName 777 | }); 778 | if (entry && !this.redisdb.closing) { 779 | let cmd = (this.testMode) ? 'multi' : 'batch'; 780 | this.redisdb[cmd]() 781 | .setex(`${redisPreKey}:${this.serviceName}:${this.instanceID}:presence`, KEY_EXPIRATION_TTL, this.instanceID) 782 | .hset(`${redisPreKey}:nodes`, this.instanceID, entry) 783 | .exec(); 784 | } 785 | } 786 | 787 | /** 788 | * @name _updateHealthCheck 789 | * @summary Update service heath. 790 | * @private 791 | * @return {undefined} 792 | */ 793 | _updateHealthCheck() { 794 | let entry = Object.assign({ 795 | updatedOn: this._getTimeStamp() 796 | }, this._getHealth()); 797 | let cmd = (this.testMode) ? 'multi' : 'batch'; 798 | this.redisdb[cmd]() 799 | .setex(`${redisPreKey}:${this.serviceName}:${this.instanceID}:health`, KEY_EXPIRATION_TTL, Utils.safeJSONStringify(entry)) 800 | .expire(`${redisPreKey}:${this.serviceName}:${this.instanceID}:health:log`, ONE_WEEK_IN_SECONDS) 801 | .exec(); 802 | } 803 | 804 | /** 805 | * @name _getHealth 806 | * @summary Retrieve server health info. 807 | * @private 808 | * @return {object} obj - object containing server info 809 | */ 810 | _getHealth() { 811 | let lines = []; 812 | let keyval = []; 813 | let map = {}; 814 | let memory = util.inspect(process.memoryUsage()); 815 | 816 | memory = memory.replace(/[\ \{\}.|\n]/g, ''); 817 | lines = memory.split(','); 818 | lines.forEach((line) => { 819 | keyval = line.split(':'); 820 | map[keyval[0]] = Number(keyval[1]); 821 | }); 822 | 823 | let uptimeInSeconds = process.uptime(); 824 | return { 825 | serviceName: this.serviceName, 826 | instanceID: this.instanceID, 827 | hostName: this.hostName, 828 | sampledOn: this._getTimeStamp(), 829 | processID: process.pid, 830 | architecture: process.arch, 831 | platform: process.platform, 832 | nodeVersion: process.version, 833 | memory: map, 834 | uptimeSeconds: uptimeInSeconds 835 | }; 836 | } 837 | 838 | /** 839 | * @name _logMessage 840 | * @summary Log a message to the service's health log queue. 841 | * @private 842 | * @throws Throws an error if this machine isn't an instance. 843 | * @event Hydra#log 844 | * @param {string} type - type of message ('error', 'info', 'debug' or user defined) 845 | * @param {string} message - message to log 846 | * @param {boolean} suppressEmit - false by default. If true then suppress log emit 847 | * @return {undefined} 848 | */ 849 | _logMessage(type, message, suppressEmit) { 850 | let errMessage = { 851 | ts: this._getTimeStamp(), 852 | serviceName: this.serviceName || 'not a service', 853 | type, 854 | processID: process.pid, 855 | msg: message 856 | }; 857 | 858 | let entry = Utils.safeJSONStringify(errMessage); 859 | debug(entry); 860 | 861 | if (!suppressEmit) { 862 | this.emit('log', errMessage); 863 | } 864 | 865 | if (entry) { 866 | // If issue is with Redis we can't use Redis to log this error. 867 | // however the above call to the application logger would be one way of detecting the issue. 868 | if (this.isService) { 869 | if (entry.toLowerCase().indexOf('redis') === -1) { 870 | if (!this.redisdb.closing) { 871 | let key = `${redisPreKey}:${this.serviceName}:${this.instanceID}:health:log`; 872 | this.redisdb.multi() 873 | .select(HYDRA_REDIS_DB) 874 | .lpush(key, entry) 875 | .ltrim(key, 0, MAX_ENTRIES_IN_HEALTH_LOG - 1) 876 | .exec(); 877 | } 878 | } 879 | } 880 | } else { 881 | console.log('Unable to log this message', type, message); 882 | } 883 | } 884 | 885 | /** 886 | * @name _getServices 887 | * @summary Retrieve a list of available services. 888 | * @private 889 | * @return {promise} promise - returns a promise 890 | */ 891 | _getServices() { 892 | return new Promise((resolve, reject) => { 893 | if (!this.redisdb) { 894 | reject(new Error('No Redis connection')); 895 | return; 896 | } 897 | this._getKeys('*:service') 898 | .then((services) => { 899 | let trans = this.redisdb.multi(); 900 | services.forEach((service) => { 901 | trans.get(service); 902 | }); 903 | trans.exec((err, result) => { 904 | if (err) { 905 | reject(err); 906 | } else { 907 | let serviceList = result.map((service) => { 908 | return Utils.safeJSONParse(service); 909 | }); 910 | resolve(serviceList); 911 | } 912 | }); 913 | }); 914 | }); 915 | } 916 | 917 | /** 918 | * @name _getServiceNodes 919 | * @summary Retrieve a list of services even if inactive. 920 | * @private 921 | * @return {promise} promise - returns a promise 922 | */ 923 | _getServiceNodes() { 924 | return new Promise((resolve, reject) => { 925 | if (!this.redisdb) { 926 | reject(new Error('No Redis connection')); 927 | return; 928 | } 929 | let now = (new Date()).getTime(); 930 | this.redisdb.hgetall(`${redisPreKey}:nodes`, (err, data) => { 931 | if (err) { 932 | reject(err); 933 | } else { 934 | let nodes = []; 935 | if (data) { 936 | Object.keys(data).forEach((entry) => { 937 | let item = Utils.safeJSONParse(data[entry]); 938 | item.elapsed = parseInt((now - (new Date(item.updatedOn)).getTime()) / ONE_SECOND); 939 | nodes.push(item); 940 | }); 941 | } 942 | resolve(nodes); 943 | } 944 | }); 945 | }); 946 | } 947 | 948 | /** 949 | * @name _findService 950 | * @summary Find a service. 951 | * @private 952 | * @param {string} name - service name - note service name is case insensitive 953 | * @return {promise} promise - which resolves with service 954 | */ 955 | _findService(name) { 956 | return new Promise((resolve, reject) => { 957 | if (!this.redisdb) { 958 | reject(new Error('No Redis connection')); 959 | return; 960 | } 961 | this.redisdb.get(`${redisPreKey}:${name}:service`, (err, result) => { 962 | if (err) { 963 | reject(err); 964 | } else { 965 | if (!result) { 966 | reject(new Error(`Can't find ${name} service`)); 967 | } else { 968 | let js = Utils.safeJSONParse(result); 969 | resolve(js); 970 | } 971 | } 972 | }); 973 | }); 974 | } 975 | 976 | /** 977 | * @name _checkServicePresence 978 | * @summary Retrieves all the "present" service instances information. 979 | * @description Differs from getServicePresence (which calls this one) 980 | * in that this performs only bare minimum fatal error checking that 981 | * would throw a reject(). This is useful when it's expected to perhaps 982 | * have some dead serivces, etc. as used in getServiceHealthAll() 983 | * for example. 984 | * @param {string} [name=our service name] - service name - note service name is case insensitive 985 | * @return {promise} promise - which resolves with a randomized service presence array or else 986 | * a reject() if a "fatal" error occured (Redis error for example) 987 | */ 988 | _checkServicePresence(name) { 989 | name = name || this._getServiceName(); 990 | return new Promise((resolve, reject) => { 991 | let cacheKey = `checkServicePresence:${name}`; 992 | let cachedValue = this.internalCache.get(cacheKey); 993 | if (cachedValue) { 994 | // Re-randomized the array each call to make sure we return a good 995 | // random set each time we access the cache... no need to store 996 | // the new random array again since it will just be randomzied again next call 997 | Utils.shuffleArray(cachedValue); 998 | resolve(cachedValue); 999 | return; 1000 | } 1001 | this._getKeys(`*:${name}:*:presence`) 1002 | .then((instances) => { 1003 | if (instances.length === 0) { 1004 | resolve([]); 1005 | return; 1006 | } 1007 | let trans = this.redisdb.multi(); 1008 | instances.forEach((instance) => { 1009 | let instanceId = instance.split(':')[3]; 1010 | trans.hget(`${redisPreKey}:nodes`, instanceId); 1011 | }); 1012 | trans.exec((err, result) => { 1013 | if (err) { 1014 | reject(err); 1015 | } else { 1016 | let instanceList = []; 1017 | result.forEach((instance) => { 1018 | if (instance) { 1019 | let instanceObj = Utils.safeJSONParse(instance); 1020 | if (instanceObj) { 1021 | instanceObj.updatedOnTS = (new Date(instanceObj.updatedOn).getTime()); 1022 | } 1023 | instanceList.push(instanceObj); 1024 | } 1025 | }); 1026 | if (instanceList.length) { 1027 | Utils.shuffleArray(instanceList); 1028 | this.internalCache.put(cacheKey, instanceList, KEY_EXPIRATION_TTL); 1029 | } 1030 | resolve(instanceList); 1031 | } 1032 | }); 1033 | }); 1034 | }); 1035 | } 1036 | 1037 | /** 1038 | * @name getServicePresence 1039 | * @summary Retrieve a service / instance's presence info. 1040 | * @private 1041 | * @param {string} name - service name - note service name is case insensitive 1042 | * @return {promise} promise - which resolves with service presence 1043 | */ 1044 | _getServicePresence(name) { 1045 | if (name === undefined) { 1046 | name = this._getServiceName(); 1047 | } 1048 | return new Promise((resolve, reject) => { 1049 | return this._checkServicePresence(name) 1050 | .then((result) => { 1051 | if (result === null) { 1052 | let msg = `Service instance for ${name} is unavailable`; 1053 | this._logMessage('error', msg); 1054 | reject(new Error(msg)); 1055 | } else { 1056 | resolve(result); 1057 | } 1058 | }) 1059 | .catch((err) => { 1060 | reject(err); 1061 | }); 1062 | }); 1063 | } 1064 | 1065 | /** 1066 | * @name _getServiceHealth 1067 | * @summary Retrieve the health status of an instance service. 1068 | * @private 1069 | * @param {string} name - name of instance service. 1070 | * @description If not specified then the current instance is assumed. - note service name is case insensitive. 1071 | * @return {promise} promise - a promise resolving to the instance's health info 1072 | */ 1073 | _getServiceHealth(name) { 1074 | if (name === undefined && !this.isService) { 1075 | let err = new Error('getServiceHealth() failed. Cant get health log since this machine isn\'t a instance.'); 1076 | throw err; 1077 | } 1078 | if (name === undefined) { 1079 | name = this._getServiceName(); 1080 | } 1081 | return new Promise((resolve, reject) => { 1082 | let cacheKey = `getServiceHealth:${name}`; 1083 | let cachedValue = this.internalCache.get(cacheKey); 1084 | if (cachedValue) { 1085 | resolve(cachedValue); 1086 | return; 1087 | } 1088 | this._getKeys(`*:${name}:*:health`) 1089 | .then((instances) => { 1090 | if (instances.length === 0) { 1091 | resolve([]); 1092 | return; 1093 | } 1094 | let trans = this.redisdb.multi(); 1095 | instances.forEach((instance) => { 1096 | trans.get(instance); 1097 | }); 1098 | trans.exec((err, result) => { 1099 | if (err) { 1100 | reject(err); 1101 | } else { 1102 | let instanceList = result.map((instance) => { 1103 | return Utils.safeJSONParse(instance); 1104 | }); 1105 | this.internalCache.put(cacheKey, instanceList, KEY_EXPIRATION_TTL); 1106 | resolve(instanceList); 1107 | } 1108 | }); 1109 | }); 1110 | }); 1111 | } 1112 | 1113 | /** 1114 | * @name _getInstanceID 1115 | * @summary Return the instance id for this process 1116 | * @return {number} id - instanceID 1117 | */ 1118 | _getInstanceID() { 1119 | return this.instanceID; 1120 | } 1121 | 1122 | /** 1123 | * @name _getInstanceVersion 1124 | * @summary Return the version of this instance 1125 | * @return {number} version - instance version 1126 | */ 1127 | _getInstanceVersion() { 1128 | return this.serviceVersion; 1129 | } 1130 | 1131 | /** 1132 | * @name _getServiceHealthLog 1133 | * @summary Get this service's health log. 1134 | * @private 1135 | * @throws Throws an error if this machine isn't a instance 1136 | * @param {string} name - name of instance service. If not specified then the current instance is assumed. 1137 | * @return {promise} promise - resolves to log entries 1138 | */ 1139 | _getServiceHealthLog(name) { 1140 | if (name === undefined && !this.isService) { 1141 | let err = new Error('getServiceHealthLog() failed. Can\'t get health log since this machine isn\'t an instance.'); 1142 | throw err; 1143 | } 1144 | if (name === undefined) { 1145 | name = this._getServiceName(); 1146 | } 1147 | return new Promise((resolve, reject) => { 1148 | this._getKeys(`*:${name}:*:health:log`) 1149 | .then((instances) => { 1150 | if (instances.length === 0) { 1151 | resolve([]); 1152 | return; 1153 | } 1154 | let trans = this.redisdb.multi(); 1155 | instances.forEach((instance) => { 1156 | trans.lrange(instance, 0, MAX_ENTRIES_IN_HEALTH_LOG - 1); 1157 | }); 1158 | trans.exec((err, result) => { 1159 | if (err) { 1160 | reject(err); 1161 | } else { 1162 | let response = []; 1163 | if (result && result.length > 0) { 1164 | result = result[0]; 1165 | result.forEach((entry) => { 1166 | response.push(Utils.safeJSONParse(entry)); 1167 | }); 1168 | } 1169 | resolve(response); 1170 | } 1171 | }); 1172 | }); 1173 | }); 1174 | } 1175 | 1176 | /** 1177 | * @name _getServiceHealthAll 1178 | * @summary Retrieve the health status of all instance services. 1179 | * @private 1180 | * @return {promise} promise - resolves with an array of objects containing instance health information. 1181 | */ 1182 | _getServiceHealthAll() { 1183 | return new Promise((resolve, reject) => { 1184 | if (!this.redisdb) { 1185 | reject(new Error('No Redis connection')); 1186 | return; 1187 | } 1188 | this._getServices() 1189 | .then((services) => { 1190 | let listOfPromises = []; 1191 | services.forEach((service) => { 1192 | let serviceName = service.serviceName; 1193 | listOfPromises.push(this._getServiceHealth(serviceName)); 1194 | listOfPromises.push(this._getServiceHealthLog(serviceName)); 1195 | listOfPromises.push(this._checkServicePresence(serviceName)); 1196 | }); 1197 | return Promise.all(listOfPromises); 1198 | }) 1199 | .then((values) => { 1200 | let response = []; 1201 | for (let i = 0; i < values.length; i += 3) { 1202 | response.push({ 1203 | health: values[i], 1204 | log: values[i + 1], 1205 | presence: values[i + 2] 1206 | }); 1207 | } 1208 | resolve(response); 1209 | }) 1210 | .catch((err) => { 1211 | reject(err); 1212 | }); 1213 | }); 1214 | } 1215 | 1216 | /** 1217 | * @name _chooseServiceInstance 1218 | * @summary Choose an instance from a list of service instances. 1219 | * @private 1220 | * @param {array} instanceList - array list of service instances 1221 | * @param {string} defaultInstance - default instance 1222 | * @return {object} promise - resolved or rejected 1223 | */ 1224 | _chooseServiceInstance(instanceList, defaultInstance) { 1225 | return new Promise((resolve, reject) => { 1226 | let instance; 1227 | 1228 | if (defaultInstance) { 1229 | for (let i = 0; i < instanceList.length; i++) { 1230 | if (instanceList[i].instanceID === defaultInstance) { 1231 | instance = instanceList[i]; 1232 | break; 1233 | } 1234 | } 1235 | } 1236 | 1237 | instance = instance || instanceList[0]; 1238 | this.redisdb.get(`${redisPreKey}:${instance.serviceName}:${instance.instanceID}:presence`, (err, _result) => { 1239 | if (err) { 1240 | reject(err); 1241 | } else { 1242 | this.redisdb.hget(`${redisPreKey}:nodes`, instance.instanceID, (err, result) => { 1243 | if (err) { 1244 | reject(err); 1245 | } else { 1246 | resolve(Utils.safeJSONParse(result)); 1247 | } 1248 | }); 1249 | } 1250 | }); 1251 | }); 1252 | } 1253 | 1254 | /** 1255 | * @name _tryAPIRequest 1256 | * @summary Attempt an API request to a hydra service. 1257 | * @description 1258 | * @param {array} instanceList - array of service instance objects 1259 | * @param {object} parsedRoute - parsed route 1260 | * @param {object} umfmsg - UMF message 1261 | * @param {function} resolve - promise resolve function 1262 | * @param {function} reject - promise reject function 1263 | * @param {object} sendOpts - serverResponse.send options 1264 | * @return {undefined} 1265 | */ 1266 | _tryAPIRequest(instanceList, parsedRoute, umfmsg, resolve, reject, sendOpts) { 1267 | let instance; 1268 | 1269 | if (parsedRoute) { 1270 | for (let i = 0; i < instanceList.length; i++) { 1271 | if (instanceList[i].instanceID === parsedRoute.instance) { 1272 | instance = instanceList[i]; 1273 | break; 1274 | } 1275 | } 1276 | } 1277 | 1278 | instance = instance || instanceList[0]; 1279 | 1280 | this.redisdb.get(`${redisPreKey}:${instance.serviceName}:${instance.instanceID}:presence`, (err, _result) => { 1281 | if (err) { 1282 | this.emit('metric', `service:unavailable|${instance.serviceName}|${instance.instanceID}|presence:not:found`); 1283 | reject(err); 1284 | } else { 1285 | this.redisdb.hget(`${redisPreKey}:nodes`, instance.instanceID, (err, result) => { 1286 | if (err) { 1287 | this.emit('metric', `service:unavailable|${instance.serviceName}|${instance.instanceID}|instance:not:found`); 1288 | reject(err); 1289 | } else { 1290 | instance = Utils.safeJSONParse(result); 1291 | let options = { 1292 | host: instance.ip, 1293 | port: instance.port, 1294 | path: parsedRoute.apiRoute, 1295 | method: parsedRoute.httpMethod.toUpperCase() 1296 | }; 1297 | let preHeaders = {}; 1298 | if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') { 1299 | preHeaders['content-type'] = 'application/json'; 1300 | } 1301 | options.headers = Object.assign(preHeaders, umfmsg.headers); 1302 | if (umfmsg.authorization) { 1303 | options.headers.Authorization = umfmsg.authorization; 1304 | } 1305 | if (umfmsg.timeout) { 1306 | options.timeout = umfmsg.timeout; 1307 | } 1308 | options.body = Utils.safeJSONStringify(umfmsg.body); 1309 | serverRequest.send(Object.assign(options, sendOpts)) 1310 | .then((res) => { 1311 | if (res.payLoad && res.headers['content-type'] && res.headers['content-type'].indexOf('json') > -1) { 1312 | res = Object.assign(res, Utils.safeJSONParse(res.payLoad.toString('utf8'))); 1313 | delete res.payLoad; 1314 | } 1315 | resolve(serverResponse.createResponseObject(res.statusCode, res)); 1316 | }) 1317 | .catch((err) => { 1318 | instanceList.shift(); 1319 | if (instanceList.length === 0) { 1320 | this.emit('metric', `service:unavailable|${instance.serviceName}|${instance.instanceID}|${err.message}`); 1321 | this.emit('metric', `service:unavailable|${instance.serviceName}|${instance.instanceID}|attempts:exhausted`); 1322 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVICE_UNAVAILABLE, `An instance of ${instance.serviceName} is unavailable`)); 1323 | } else { 1324 | this.emit('metric', `service:unavailable|${instance.serviceName}|${instance.instanceID}|${err.message}`); 1325 | this._tryAPIRequest(instanceList, parsedRoute, umfmsg, resolve, reject, sendOpts); 1326 | } 1327 | }); 1328 | } 1329 | }); 1330 | } 1331 | }); 1332 | } 1333 | 1334 | /** 1335 | * @name _makeAPIRequest 1336 | * @summary Makes an API request to a hydra service. 1337 | * @description If the service isn't present and the message object has its 1338 | * message.body.fallbackToQueue value set to true, then the 1339 | * message will be sent to the services message queue. 1340 | * @param {object} message - UMF formatted message 1341 | * @param {object} sendOpts - serverResponse.send options 1342 | * @return {promise} promise - response from API in resolved promise or 1343 | * error in rejected promise. 1344 | */ 1345 | _makeAPIRequest(message, sendOpts = { }) { 1346 | return new Promise((resolve, reject) => { 1347 | let umfmsg = UMFMessage.createMessage(message); 1348 | if (!umfmsg.validate()) { 1349 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_BAD_REQUEST, UMF_INVALID_MESSAGE)); 1350 | return; 1351 | } 1352 | 1353 | let parsedRoute = UMFMessage.parseRoute(umfmsg.to); 1354 | if (parsedRoute.error) { 1355 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_BAD_REQUEST, parsedRoute.error)); 1356 | return; 1357 | } 1358 | 1359 | if (!parsedRoute.httpMethod) { 1360 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_BAD_REQUEST, 'HTTP method not specified in `to` field')); 1361 | return; 1362 | } 1363 | 1364 | if (parsedRoute.apiRoute === '') { 1365 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_BAD_REQUEST, 'message `to` field does not specify a valid route')); 1366 | return; 1367 | } 1368 | 1369 | this._getServicePresence(parsedRoute.serviceName) 1370 | .then((instances) => { 1371 | if (instances.length === 0) { 1372 | this.emit('metric', `service:unavailable|${parsedRoute.serviceName}`); 1373 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVICE_UNAVAILABLE, `Unavailable ${parsedRoute.serviceName} instances`)); 1374 | return; 1375 | } 1376 | this._tryAPIRequest(instances, parsedRoute, umfmsg, resolve, reject, sendOpts); 1377 | return 0; 1378 | }) 1379 | .catch((err) => { 1380 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVER_ERROR, err.message)); 1381 | }); 1382 | }); 1383 | } 1384 | 1385 | /** 1386 | * @name _sendMessageThroughChannel 1387 | * @summary Sends a message to a Redis pubsub channel 1388 | * @param {string} channel - channel name 1389 | * @param {object} message - UMF formatted message object 1390 | * @return {undefined} 1391 | */ 1392 | _sendMessageThroughChannel(channel, message) { 1393 | if (!this.publishChannel) { 1394 | this.publishChannel = this.redisdb.duplicate(); 1395 | } 1396 | if (this.publishChannel) { 1397 | let msg = UMFMessage.createMessage(message); 1398 | let strMessage = Utils.safeJSONStringify(msg.toShort()); 1399 | this.publishChannel.publish(channel, strMessage); 1400 | } 1401 | } 1402 | 1403 | /** 1404 | * @name sendMessage 1405 | * @summary Sends a message to an instances of a hydra service. 1406 | * @param {object} message - UMF formatted message object 1407 | * @return {object} promise - resolved promise if sent or 1408 | * HTTP error in resolve() if something bad happened 1409 | */ 1410 | _sendMessage(message) { 1411 | return new Promise((resolve, _reject) => { 1412 | let { 1413 | serviceName, 1414 | instance 1415 | } = UMFMessage.parseRoute(message.to); 1416 | this._getServicePresence(serviceName) 1417 | .then((instances) => { 1418 | if (instances.length === 0) { 1419 | let msg = `Unavailable ${serviceName} instances`; 1420 | this._logMessage('error', msg); 1421 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVICE_UNAVAILABLE, msg)); 1422 | return; 1423 | } 1424 | // Did the user specify a specific service instance to use? 1425 | if (instance && instance !== '') { 1426 | // Make sure supplied instance actually exists in the array 1427 | let found = instances.filter((entry) => entry.instanceID === instance); 1428 | if (found.length > 0) { 1429 | this._sendMessageThroughChannel(`${mcMessageKey}:${serviceName}:${instance}`, message); 1430 | } else { 1431 | let msg = `Unavailable ${serviceName} instance named ${instance}`; 1432 | this._logMessage('error', msg); 1433 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVICE_UNAVAILABLE, msg)); 1434 | return; 1435 | } 1436 | } else { 1437 | // Send to a random service. It's random beause currently _getServicePresence() 1438 | // returns a shuffled array. 1439 | let serviceInstance = instances[0]; 1440 | this._sendMessageThroughChannel(`${mcMessageKey}:${serviceName}:${serviceInstance.instanceID}`, message); 1441 | } 1442 | resolve(); 1443 | }) 1444 | .catch((err) => { 1445 | let msg = err.message; 1446 | this._logMessage('error', msg); 1447 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVER_ERROR, msg)); 1448 | }); 1449 | }); 1450 | } 1451 | 1452 | /** 1453 | * @name _sendReplyMessage 1454 | * @summary Sends a reply message based on the original message received. 1455 | * @param {object} originalMessage - UMF formatted message object 1456 | * @param {object} messageResponse - UMF formatted message object 1457 | * @return {object} promise - resolved promise if sent or 1458 | * error in rejected promise. 1459 | */ 1460 | _sendReplyMessage(originalMessage, messageResponse) { 1461 | let longOriginalMessage = UMFMessage 1462 | .createMessage(originalMessage) 1463 | .toJSON(); 1464 | let longMessageResponse = UMFMessage 1465 | .createMessage(messageResponse) 1466 | .toJSON(); 1467 | let reply = Object.assign(longOriginalMessage, { 1468 | rmid: longOriginalMessage.mid, 1469 | to: longOriginalMessage.from, 1470 | from: longOriginalMessage.to 1471 | }, longMessageResponse); 1472 | if (longOriginalMessage.via) { 1473 | reply.to = longOriginalMessage.via; 1474 | } 1475 | if (longOriginalMessage.forward) { 1476 | reply.forward = longOriginalMessage.forward; 1477 | } 1478 | return this._sendMessage(reply); 1479 | } 1480 | 1481 | /** 1482 | * @name sendBroadcastMessage 1483 | * @summary Sends a message to all present instances of a hydra service. 1484 | * @param {object} message - UMF formatted message object 1485 | * @return {object} promise - resolved promise if sent or 1486 | * error in rejected promise. 1487 | */ 1488 | _sendBroadcastMessage(message) { 1489 | return new Promise((resolve, _reject) => { 1490 | let { 1491 | serviceName 1492 | } = UMFMessage.parseRoute(message.to); 1493 | this._getServicePresence(serviceName) 1494 | .then((instances) => { 1495 | if (instances.length === 0) { 1496 | if (serviceName !== 'hydra-router') { 1497 | let msg = `Unavailable ${serviceName} instances`; 1498 | this._logMessage('error', msg); 1499 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVICE_UNAVAILABLE, msg)); 1500 | } else { 1501 | resolve(); 1502 | } 1503 | return; 1504 | } 1505 | this._sendMessageThroughChannel(`${mcMessageKey}:${serviceName}`, message); 1506 | resolve(); 1507 | }) 1508 | .catch((err) => { 1509 | let msg = err.message; 1510 | this._logMessage('error', msg); 1511 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_SERVER_ERROR, msg)); 1512 | }); 1513 | }); 1514 | } 1515 | 1516 | /** 1517 | * @name _queueMessage 1518 | * @summary Queue a message 1519 | * @param {object} message - UMF message to queue 1520 | * @return {promise} promise - resolving to the message that was queued or a rejection. 1521 | */ 1522 | _queueMessage(message) { 1523 | return new Promise((resolve, reject) => { 1524 | let umfmsg = UMFMessage.createMessage(message); 1525 | if (!umfmsg.validate()) { 1526 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_BAD_REQUEST, UMF_INVALID_MESSAGE)); 1527 | return; 1528 | } 1529 | 1530 | let parsedRoute = UMFMessage.parseRoute(umfmsg.to); 1531 | if (parsedRoute.error) { 1532 | resolve(this._createServerResponseWithReason(ServerResponse.HTTP_BAD_REQUEST, parsedRoute.error)); 1533 | return; 1534 | } 1535 | 1536 | let serviceName = parsedRoute.serviceName; 1537 | this.redisdb.lpush(`${redisPreKey}:${serviceName}:mqrecieved`, Utils.safeJSONStringify(umfmsg.toShort()), (err, _data) => { 1538 | if (err) { 1539 | reject(err); 1540 | } else { 1541 | resolve(message); 1542 | } 1543 | }); 1544 | }); 1545 | } 1546 | 1547 | /** 1548 | * @name _getQueuedMessage 1549 | * @summary retrieve a queued message 1550 | * @param {string} serviceName who's queue might provide a message 1551 | * @return {promise} promise - resolving to the message that was dequeued or a rejection. 1552 | */ 1553 | _getQueuedMessage(serviceName) { 1554 | return new Promise((resolve, reject) => { 1555 | this.redisdb.rpoplpush(`${redisPreKey}:${serviceName}:mqrecieved`, `${redisPreKey}:${serviceName}:mqinprogress`, (err, data) => { 1556 | if (err) { 1557 | reject(err); 1558 | } else { 1559 | let msg = Utils.safeJSONParse(data); 1560 | resolve(msg); 1561 | } 1562 | }); 1563 | }); 1564 | } 1565 | 1566 | /** 1567 | * @name _markQueueMessage 1568 | * @summary Mark a queued message as either completed or not 1569 | * @param {object} message - message in question 1570 | * @param {boolean} completed - (true / false) 1571 | * @param {string} reason - if not completed this is the reason processing failed 1572 | * @return {promise} promise - resolving to the message that was dequeued or a rejection. 1573 | */ 1574 | _markQueueMessage(message, completed, reason) { 1575 | let serviceName = this._getServiceName(); 1576 | return new Promise((resolve, reject) => { 1577 | let strMessage = Utils.safeJSONStringify(message); 1578 | this.redisdb.lrem(`${redisPreKey}:${serviceName}:mqinprogress`, -1, strMessage, (err, _data) => { 1579 | if (err) { 1580 | reject(err); 1581 | } else { 1582 | if (message.bdy) { 1583 | message.bdy.reason = reason || 'reason not provided'; 1584 | } else if (message.body) { 1585 | message.body.reason = reason || 'reason not provided'; 1586 | } 1587 | if (completed) { 1588 | resolve(message); 1589 | } else { 1590 | strMessage = Utils.safeJSONStringify(message); 1591 | this.redisdb.rpush(`${redisPreKey}:${serviceName}:mqincomplete`, strMessage, (err, data) => { 1592 | if (err) { 1593 | reject(err); 1594 | } else { 1595 | resolve(data); 1596 | } 1597 | }); 1598 | } 1599 | } 1600 | }); 1601 | }); 1602 | } 1603 | 1604 | /** 1605 | * @name _hasServicePresence 1606 | * @summary Indicate if a service has presence. 1607 | * @description Indicates if a service has presence, meaning the 1608 | * service is running in at least one node. 1609 | * @param {string} name - service name - note service name is case insensitive 1610 | * @return {promise} promise - which resolves with TRUE if presence is found, FALSE otherwise 1611 | */ 1612 | _hasServicePresence(name) { 1613 | name = name || this._getServiceName(); 1614 | return new Promise((resolve, reject) => { 1615 | this._getKeys(`*:${name}:*:presence`) 1616 | .then((instances) => { 1617 | resolve(instances.length !== 0); 1618 | }) 1619 | .catch(reject); 1620 | }); 1621 | } 1622 | 1623 | /** 1624 | * @name _getConfig 1625 | * @summary retrieve a stored configuration file 1626 | * @param {string} label - service label containing servicename and version: such as myservice:0.0.1 1627 | * @return {promise} promise - resolving to a configuration file in object format 1628 | */ 1629 | _getConfig(label) { 1630 | return new Promise((resolve, reject) => { 1631 | let parts = label.split(':'); 1632 | if (parts.length !== 2) { 1633 | let msg = 'label not in this form: myservice:0.1.1.'; 1634 | this._logMessage('error', msg); 1635 | reject(new Error(msg)); 1636 | } 1637 | this.redisdb.hget(`${redisPreKey}:${parts[0]}:configs`, parts[1], (err, result) => { 1638 | if (err) { 1639 | let msg = 'Unable to set :configs key in Redis db.'; 1640 | this._logMessage('error', msg); 1641 | reject(new Error(msg)); 1642 | } else { 1643 | resolve(Utils.safeJSONParse(result)); 1644 | } 1645 | }); 1646 | }); 1647 | } 1648 | 1649 | /** 1650 | * @name _putConfig 1651 | * @summary store a configuration file 1652 | * @param {string} label - service label containing servicename and version: such as myservice:0.0.1 1653 | * @param {object} config - configuration object 1654 | * @return {promise} promise - resolving or rejecting. 1655 | */ 1656 | _putConfig(label, config) { 1657 | return new Promise((resolve, reject) => { 1658 | let parts = label.split(':'); 1659 | if (parts.length !== 2) { 1660 | let msg = 'label not in this form: myservice:0.1.1.'; 1661 | this._logMessage('error', msg); 1662 | reject(new Error(msg)); 1663 | } 1664 | this.redisdb.hset(`${redisPreKey}:${parts[0]}:configs`, `${parts[1]}`, Utils.safeJSONStringify(config), (err, _result) => { 1665 | if (err) { 1666 | reject(new Error('Unable to set :configs key in Redis db.')); 1667 | } else { 1668 | resolve(); 1669 | } 1670 | }); 1671 | }); 1672 | } 1673 | 1674 | /** 1675 | * @name _listConfig 1676 | * @summary Return a list of config keys 1677 | * @param {string} serviceName - name of service 1678 | * @return {promise} promise - resolving or rejecting. 1679 | */ 1680 | _listConfig(serviceName) { 1681 | return new Promise((resolve, reject) => { 1682 | this.redisdb.hkeys(`${redisPreKey}:${serviceName}:configs`, (err, result) => { 1683 | if (err) { 1684 | let msg = 'Unable to retrieve :config keys from Redis db.'; 1685 | this._logMessage('error', msg); 1686 | reject(new Error(msg)); 1687 | } else { 1688 | if (result) { 1689 | result.sort(); 1690 | resolve(result.map((item) => `${serviceName}:${item}`)); 1691 | } else { 1692 | resolve([]); 1693 | } 1694 | } 1695 | }); 1696 | }); 1697 | } 1698 | 1699 | /** 1700 | * @name _getClonedRedisClient 1701 | * @summary get a Redis client connection which points to the same Redis server that hydra is using 1702 | * @param {object} [options] - override options from original createClient call 1703 | * @param {function} [callback] - callback for async connect 1704 | * @return {object} - Redis Client 1705 | */ 1706 | _getClonedRedisClient(options, callback) { 1707 | return this.redisdb.duplicate(options, callback); 1708 | } 1709 | 1710 | /** 1711 | * @name _getUMFMessageHelper 1712 | * @summary returns UMF object helper 1713 | * @return {object} helper - UMF helper 1714 | */ 1715 | _getUMFMessageHelper() { 1716 | return require('./lib/umfmessage'); 1717 | } 1718 | 1719 | /** 1720 | * @name _getServerRequestHelper 1721 | * @summary returns ServerRequest helper 1722 | * @return {object} helper - service request helper 1723 | */ 1724 | _getServerRequestHelper() { 1725 | return require('./lib/server-request'); 1726 | } 1727 | 1728 | /** 1729 | * @name _getServerResponseHelper 1730 | * @summary returns ServerResponse helper 1731 | * @return {object} helper - service response helper 1732 | */ 1733 | _getServerResponseHelper() { 1734 | return require('./lib/server-response'); 1735 | } 1736 | 1737 | /** 1738 | * @name _getUtilsHelper 1739 | * @summary returns a utils helper 1740 | * @return {object} helper - utils helper 1741 | */ 1742 | _getUtilsHelper() { 1743 | return require('./lib/utils'); 1744 | } 1745 | 1746 | /** 1747 | * @name _getConfigHelper 1748 | * @summary returns a config helper 1749 | * @return {object} helper - config helper 1750 | */ 1751 | _getConfigHelper() { 1752 | return require('./lib/config'); 1753 | } 1754 | 1755 | /** ************************************************************** 1756 | * Hydra private utility functions. 1757 | * ***************************************************************/ 1758 | 1759 | /** 1760 | * @name _createServerResponseWithReason 1761 | * @summary Create a server response using an HTTP code and reason 1762 | * @param {number} httpCode - code using ServerResponse.HTTP_XXX 1763 | * @param {string} reason - reason description 1764 | * @return {object} response - response object for use with promise resolve and reject calls 1765 | */ 1766 | _createServerResponseWithReason(httpCode, reason) { 1767 | return serverResponse.createResponseObject(httpCode, { 1768 | result: { 1769 | reason: reason 1770 | } 1771 | }); 1772 | } 1773 | 1774 | /** 1775 | * @name _parseServicePortConfig 1776 | * @summary Parse and process given port data in config 1777 | * @param {mixed} port - configured port 1778 | * @return {promise} promise - resolving with unassigned port, rejecting when no free port is found 1779 | */ 1780 | _parseServicePortConfig(port) { 1781 | return new Promise((resolve, reject) => { 1782 | // No port given, get unassigned port from standard ranges 1783 | if (typeof port === 'undefined' || !port || port == 0) { 1784 | port = '1024-65535'; 1785 | } else if (! /-|,/.test(port.toString())) { 1786 | // Specific port given, skip free port check 1787 | resolve(port.toString()); 1788 | return; 1789 | } 1790 | let portRanges = port.toString().split(',') 1791 | .map((p) => { 1792 | p = p.trim(); 1793 | const ipRe = '(?:6553[0-5]|655[0-2][0-9]|65[0-4][0-9]{2}|6[0-4][0-9]{3}|[1-5][0-9]{4}|[1-9][0-9]{1,3}|[1-9])'; 1794 | let matches = p.match(new RegExp(`^${ipRe}-${ipRe}$`, 'g')); 1795 | if (matches !== null) { 1796 | return p; 1797 | } else { 1798 | matches = p.match(new RegExp(`^${ipRe}$`, 'g')); 1799 | if (matches !== null) { 1800 | return p; 1801 | } 1802 | } 1803 | return null; 1804 | }) 1805 | .filter((p) => p != null); 1806 | let receivedCallBacks = 0; 1807 | if (portRanges.length == 0) { 1808 | let msg = 'servicePort configuration does not contain valid port(s)'; 1809 | this._logMessage('error', msg); 1810 | reject(msg); 1811 | return; 1812 | } 1813 | portRanges.forEach((rangeToCheck, _index) => { 1814 | let min = 0; 1815 | let max = 0; 1816 | let foundRanges = rangeToCheck.split('-'); 1817 | if (foundRanges.length == 1) { 1818 | min = foundRanges[0]; 1819 | max = min; 1820 | } else { 1821 | min = foundRanges[0]; 1822 | max = foundRanges[1]; 1823 | } 1824 | this._getUnassignedRandomServicePort(parseInt(min), parseInt(max), (port) => { 1825 | receivedCallBacks++; 1826 | if (port !== 0) { 1827 | resolve(port); 1828 | return; 1829 | } else { 1830 | if (receivedCallBacks === portRanges.length) { 1831 | let msg = 'No available service port in provided port range'; 1832 | this._logMessage('error', msg); 1833 | reject(msg); 1834 | } 1835 | } 1836 | }); 1837 | }); 1838 | }); 1839 | } 1840 | 1841 | /** 1842 | * @name _getUnassignedRandomServicePort 1843 | * @summary retrieve a free service port in given range 1844 | * @param {number} min - Minimum port number, included 1845 | * @param {number} max - Maximum port number, included 1846 | * @param {function} callback - Callback function when done 1847 | * @param {array} portsTried - Ports which have been tried 1848 | * @return {undefined} 1849 | **/ 1850 | _getUnassignedRandomServicePort(min, max, callback, portsTried) { 1851 | const instance = this; 1852 | const host = this.config.serviceIP; 1853 | if (typeof portsTried === 'undefined') { 1854 | portsTried = []; 1855 | } else { 1856 | if (portsTried.length == (max - min + 1)) { 1857 | callback(0); 1858 | return; 1859 | } 1860 | } 1861 | 1862 | let port = Math.floor(Math.random() * (max - min + 1)) + min; 1863 | while (portsTried.indexOf(port) !== -1) { 1864 | port = Math.floor(Math.random() * (max - min + 1)) + min; 1865 | } 1866 | portsTried.push(port); 1867 | 1868 | const server = require('net').createServer(); 1869 | server.listen({port, host}, () => { 1870 | server.once('close', () => { 1871 | callback(port); 1872 | }); 1873 | server.close(); 1874 | }); 1875 | server.on('error', () => { 1876 | instance._getUnassignedRandomServicePort(min, max, callback, portsTried); 1877 | }); 1878 | } 1879 | 1880 | /** 1881 | * @name _createUMFMessage 1882 | * @summary Create a UMF style message. 1883 | * @description This is a helper function which helps format a UMF style message. 1884 | * The caller is responsible for ensuring that required fields such as 1885 | * "to", "from" and "body" are provided either before or after using 1886 | * this function. 1887 | * @param {object} message - optional message overrides. 1888 | * @return {object} message - a UMF formatted message. 1889 | */ 1890 | _createUMFMessage(message) { 1891 | return UMFMessage.createMessage(message); 1892 | } 1893 | 1894 | /** 1895 | * @name _getTimeStamp 1896 | * @summary Retrieve an ISO 8601 timestamp. 1897 | * @return {string} timestamp - ISO 8601 timestamp 1898 | */ 1899 | _getTimeStamp() { 1900 | return new Date().toISOString(); 1901 | } 1902 | 1903 | /** 1904 | * @name _getParentPackageJSONVersion 1905 | * @summary Retrieve the version from the host app's package.json file. 1906 | * @return {string} version - package version 1907 | */ 1908 | _getParentPackageJSONVersion() { 1909 | let version; 1910 | try { 1911 | const path = require('path'); 1912 | let fpath = `${path.dirname(process.argv[1])}/package.json`; 1913 | version = require(fpath).version; 1914 | } catch (e) { 1915 | version = 'unspecified'; 1916 | } 1917 | return version; 1918 | } 1919 | } 1920 | 1921 | /** ************************************************************** 1922 | * Hydra interface class 1923 | * ***************************************************************/ 1924 | 1925 | /** 1926 | * @name IHydra 1927 | * @summary Interface to Hydra, can provide microservice functionality or be used to monitor microservices. 1928 | * @fires Hydra#log 1929 | * @fires Hydra#message 1930 | */ 1931 | class IHydra extends Hydra { 1932 | /** 1933 | * @name constructor 1934 | */ 1935 | constructor() { 1936 | super(); 1937 | } 1938 | 1939 | /** 1940 | * @name init 1941 | * @summary Initialize Hydra with config object. 1942 | * @param {mixed} config - a string with a path to a configuration file or an 1943 | * object containing hydra specific keys/values 1944 | * @param {boolean} testMode - whether hydra is being started in unit test mode 1945 | * @return {object} promise - resolving if init success or rejecting otherwise 1946 | */ 1947 | init(config, testMode = false) { 1948 | return super.init(config, testMode); 1949 | } 1950 | 1951 | /** 1952 | * @name use 1953 | * @summary Use plugins 1954 | * @param {array} plugins - plugins to process 1955 | * @return {undefined} 1956 | */ 1957 | use(...plugins) { 1958 | return super.use(...plugins); 1959 | } 1960 | 1961 | /** 1962 | * @name _shutdown 1963 | * @summary Shutdown hydra safely. 1964 | * @return {undefined} 1965 | */ 1966 | shutdown() { 1967 | return super._shutdown(); 1968 | } 1969 | 1970 | /** 1971 | * @name registerService 1972 | * @summary Registers this machine as a Hydra instance. 1973 | * @description This is an optional call as this module might just be used to monitor and query instances. 1974 | * @return {object} promise - resolving if registration success or rejecting otherwise 1975 | */ 1976 | registerService() { 1977 | return super._registerService(); 1978 | } 1979 | 1980 | /** 1981 | * @name getServiceName 1982 | * @summary Retrieves the service name of the current instance. 1983 | * @throws Throws an error if this machine isn't a instance. 1984 | * @return {string} serviceName - returns the service name. 1985 | */ 1986 | getServiceName() { 1987 | return super._getServiceName(); 1988 | } 1989 | 1990 | /** 1991 | * @name getServices 1992 | * @summary Retrieve a list of available instance services. 1993 | * @return {promise} promise - returns a promise which resolves to an array of objects. 1994 | */ 1995 | getServices() { 1996 | return super._getServices(); 1997 | } 1998 | 1999 | /** 2000 | * @name getServiceNodes 2001 | * @summary Retrieve a list of services even if inactive. 2002 | * @return {promise} promise - returns a promise 2003 | */ 2004 | getServiceNodes() { 2005 | return super._getServiceNodes(); 2006 | } 2007 | 2008 | /** 2009 | * @name findService 2010 | * @summary Find a service. 2011 | * @param {string} name - service name - note service name is case insensitive 2012 | * @return {promise} promise - which resolves with service 2013 | */ 2014 | findService(name) { 2015 | return super._findService(name); 2016 | } 2017 | 2018 | /** 2019 | * @name getServicePresence 2020 | * @summary Retrieve a service / instance's presence info. 2021 | * @param {string} name - service name - note service name is case insensitive 2022 | * @return {promise} promise - which resolves with service presence 2023 | */ 2024 | getServicePresence(name) { 2025 | return super._getServicePresence(name); 2026 | } 2027 | 2028 | /** 2029 | * @name getInstanceID 2030 | * @summary Return the instance id for this process 2031 | * @return {number} id - instanceID 2032 | */ 2033 | getInstanceID() { 2034 | return super._getInstanceID(); 2035 | } 2036 | 2037 | /** 2038 | * @name getInstanceVersion 2039 | * @summary Return the version of this instance 2040 | * @return {number} version - instance version 2041 | */ 2042 | getInstanceVersion() { 2043 | return super._getInstanceVersion(); 2044 | } 2045 | 2046 | /** 2047 | * @name getHealth 2048 | * @summary Retrieve service health info. 2049 | * @private 2050 | * @return {object} obj - object containing service health info 2051 | */ 2052 | getHealth() { 2053 | return this._getHealth(); 2054 | } 2055 | 2056 | /** 2057 | * @name sendToHealthLog 2058 | * @summary Log a message to the service instance's health log queue. 2059 | * @private 2060 | * @throws Throws an error if this machine isn't a instance. 2061 | * @param {string} type - type of message ('error', 'info', 'debug' or user defined) 2062 | * @param {string} message - message to log 2063 | * @param {boolean} suppressEmit - false by default. If true then suppress log emit 2064 | * @return {undefined} 2065 | */ 2066 | sendToHealthLog(type, message, suppressEmit) { 2067 | this._logMessage(type, message, suppressEmit); 2068 | } 2069 | 2070 | /** 2071 | * @name getServiceHealthLog 2072 | * @summary Get this service's health log. 2073 | * @throws Throws an error if this machine isn't a instance 2074 | * @param {string} name - name of instance, use getName() if current service is the target. 2075 | * note service name is case insensitive. 2076 | * @return {promise} promise - resolves to log entries 2077 | */ 2078 | getServiceHealthLog(name) { 2079 | return super._getServiceHealthLog(name); 2080 | } 2081 | 2082 | /** 2083 | * @name getServiceHealthAll 2084 | * @summary Retrieve the health status of all instance services. 2085 | * @return {promise} promise - resolves with an array of objects containing instance health information. 2086 | */ 2087 | getServiceHealthAll() { 2088 | return super._getServiceHealthAll(); 2089 | } 2090 | 2091 | /** 2092 | * @name createUMFMessage 2093 | * @summary Create a UMF style message. 2094 | * @description This is a helper function which helps format a UMF style message. 2095 | * The caller is responsible for ensuring that required fields such as 2096 | * "to", "from" and "body" are provided either before or after using 2097 | * this function. 2098 | * @param {object} message - optional message overrides. 2099 | * @return {object} message - a UMF formatted message. 2100 | */ 2101 | createUMFMessage(message) { 2102 | return super._createUMFMessage(message); 2103 | } 2104 | 2105 | /** 2106 | * @name makeAPIRequest 2107 | * @summary Makes an API request to a hydra service. 2108 | * @description If the service isn't present and the message object has its 2109 | * message.body.fallbackToQueue value set to true, then the 2110 | * message will be sent to the services message queue. 2111 | * @param {object} message - UMF formatted message 2112 | * @param {object} sendOpts - serverResponse.send options 2113 | * @return {promise} promise - response from API in resolved promise or 2114 | * error in rejected promise. 2115 | */ 2116 | makeAPIRequest(message, sendOpts = { }) { 2117 | return super._makeAPIRequest(message, sendOpts); 2118 | } 2119 | 2120 | /** 2121 | * @name sendMessage 2122 | * @summary Sends a message to all present instances of a hydra service. 2123 | * @param {string | object} message - Plain string or UMF formatted message object 2124 | * @return {object} promise - resolved promise if sent or 2125 | * error in rejected promise. 2126 | */ 2127 | sendMessage(message) { 2128 | return super._sendMessage(message); 2129 | } 2130 | 2131 | /** 2132 | * @name sendReplyMessage 2133 | * @summary Sends a reply message based on the original message received. 2134 | * @param {object} originalMessage - UMF formatted message object 2135 | * @param {object} messageResponse - UMF formatted message object 2136 | * @return {object} promise - resolved promise if sent or 2137 | * error in rejected promise. 2138 | */ 2139 | sendReplyMessage(originalMessage, messageResponse) { 2140 | return super._sendReplyMessage(originalMessage, messageResponse); 2141 | } 2142 | 2143 | /** 2144 | * @name sendBroadcastMessage 2145 | * @summary Sends a message to all present instances of a hydra service. 2146 | * @param {string | object} message - Plain string or UMF formatted message object 2147 | * @return {object} promise - resolved promise if sent or 2148 | * error in rejected promise. 2149 | */ 2150 | sendBroadcastMessage(message) { 2151 | return super._sendBroadcastMessage(message); 2152 | } 2153 | 2154 | /** 2155 | * @name registerRoutes 2156 | * @summary Register routes 2157 | * @description Routes must be formatted as UMF To routes. https://github.com/cjus/umf#%20To%20field%20(routing) 2158 | * @param {array} routes - array of routes 2159 | * @return {object} Promise - resolving or rejecting 2160 | */ 2161 | registerRoutes(routes) { 2162 | return super._registerRoutes(routes); 2163 | } 2164 | 2165 | /** 2166 | * @name getAllServiceRoutes 2167 | * @summary Retrieve all service routes. 2168 | * @return {object} Promise - resolving to an object with keys and arrays of routes 2169 | */ 2170 | getAllServiceRoutes() { 2171 | return super._getAllServiceRoutes(); 2172 | } 2173 | 2174 | /** 2175 | * @name matchRoute 2176 | * @summary Matches a route path to a list of registered routes 2177 | * @private 2178 | * @param {string} routePath - a URL path to match 2179 | * @return {boolean} match - true if match, false if not 2180 | */ 2181 | matchRoute(routePath) { 2182 | return super._matchRoute(routePath); 2183 | } 2184 | 2185 | /** 2186 | * @name queueMessage 2187 | * @summary Queue a message 2188 | * @param {object} message - UMF message to queue 2189 | * @return {promise} promise - resolving to the message that was queued or a rejection. 2190 | */ 2191 | queueMessage(message) { 2192 | return super._queueMessage(message); 2193 | } 2194 | 2195 | /** 2196 | * @name getQueuedMessage 2197 | * @summary retrieve a queued message 2198 | * @param {string} serviceName who's queue might provide a message 2199 | * @return {promise} promise - resolving to the message that was dequeued or a rejection. 2200 | */ 2201 | getQueuedMessage(serviceName) { 2202 | return super._getQueuedMessage(serviceName); 2203 | } 2204 | 2205 | /** 2206 | * @name markQueueMessage 2207 | * @summary Mark a queued message as either completed or not 2208 | * @param {object} message - message in question 2209 | * @param {boolean} completed - (true / false) 2210 | * @param {string} reason - if not completed this is the reason processing failed 2211 | * @return {promise} promise - resolving to the message that was dequeued or a rejection. 2212 | */ 2213 | markQueueMessage(message, completed, reason) { 2214 | return super._markQueueMessage(message, completed, reason); 2215 | } 2216 | 2217 | /** 2218 | * @name _getConfig 2219 | * @summary retrieve a stored configuration file 2220 | * @param {string} label - service label containing servicename and version: such as myservice:0.0.1 2221 | * @return {promise} promise - resolving to a configuration file in object format 2222 | */ 2223 | getConfig(label) { 2224 | return super._getConfig(label); 2225 | } 2226 | 2227 | /** 2228 | * @name _putConfig 2229 | * @summary store a configuration file 2230 | * @param {string} label - service label containing servicename and version: such as myservice:0.0.1 2231 | * @param {object} config - configuration object 2232 | * @return {promise} promise - resolving or rejecting. 2233 | */ 2234 | putConfig(label, config) { 2235 | return super._putConfig(label, config); 2236 | } 2237 | 2238 | /** 2239 | * @name listConfig 2240 | * @summary Return a list of config keys 2241 | * @param {string} serviceName - name of service 2242 | * @return {promise} promise - resolving or rejecting. 2243 | */ 2244 | listConfig(serviceName) { 2245 | return super._listConfig(serviceName); 2246 | } 2247 | 2248 | /** 2249 | * @name hasServicePresence 2250 | * @summary Indicate if a service has presence. 2251 | * @description Indicates if a service has presence, meaning the 2252 | * service is running in at least one node. 2253 | * @param {string} name - service name - note service name is case insensitive 2254 | * @return {promise} promise - which resolves with TRUE if presence is found, FALSE otherwise 2255 | */ 2256 | hasServicePresence(name) { 2257 | return super._hasServicePresence(name); 2258 | } 2259 | 2260 | /** 2261 | * @name getClonedRedisClient 2262 | * @summary get a Redis client connection which points to the same Redis server that hydra is using 2263 | * @return {object} - Redis Client 2264 | */ 2265 | getClonedRedisClient() { 2266 | return super._getClonedRedisClient(); 2267 | } 2268 | 2269 | /** 2270 | * @name getUMFMessageHelper 2271 | * @summary returns UMF object helper 2272 | * @return {object} helper - UMF helper 2273 | */ 2274 | getUMFMessageHelper() { 2275 | return super._getUMFMessageHelper(); 2276 | } 2277 | 2278 | /** 2279 | * @name getServerRequestHelper 2280 | * @summary returns ServerRequest helper 2281 | * @return {object} helper - service request helper 2282 | */ 2283 | getServerRequestHelper() { 2284 | return super._getServerRequestHelper(); 2285 | } 2286 | 2287 | /** 2288 | * @name getServerResponseHelper 2289 | * @summary returns ServerResponse helper 2290 | * @return {object} helper - service response helper 2291 | */ 2292 | getServerResponseHelper() { 2293 | return super._getServerResponseHelper(); 2294 | } 2295 | 2296 | /** 2297 | * @name getUtilsHelper 2298 | * @summary returns a Utils helper 2299 | * @return {object} helper - utils helper 2300 | */ 2301 | getUtilsHelper() { 2302 | return super._getUtilsHelper(); 2303 | } 2304 | 2305 | /** 2306 | * @name getConfigHelper 2307 | * @summary returns a config helper 2308 | * @return {object} helper - config helper 2309 | */ 2310 | getConfigHelper() { 2311 | return super._getConfigHelper(); 2312 | } 2313 | } 2314 | 2315 | module.exports = new IHydra; 2316 | -------------------------------------------------------------------------------- /large-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/pnxtech/hydra/b3a0ac04b1259b9680cbf88b5400a254f6a8fdd9/large-logo.png -------------------------------------------------------------------------------- /lib/cache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | /** 4 | * @name Cache 5 | * @summary Internal cache helper 6 | */ 7 | class Cache { 8 | /** 9 | * @name constructor 10 | * @summary constructor 11 | * @return {undefined} 12 | */ 13 | constructor() { 14 | this.data = {}; 15 | } 16 | 17 | /** 18 | * @name put 19 | * @summary put a value in the cache 20 | * @param {string} key - key for value 21 | * @param {any} value - value associated with key 22 | * @param {number} expiration - expiration in seconds 23 | * @return {undefined} 24 | */ 25 | put(key, value, expiration = 0) { 26 | this.data[key] = { 27 | value, 28 | ts: Date.now() / 1000, 29 | expiration 30 | }; 31 | } 32 | 33 | /** 34 | * @name get 35 | * @summary get a value based on key 36 | * @param {string} key - key for value 37 | * @return {any} value - value associated with key or undefined if missing or expired 38 | */ 39 | get(key) { 40 | let item = this.data[key]; 41 | if (item) { 42 | let current = Date.now() / 1000; 43 | if (current > (item.ts + item.expiration)) { 44 | this.data[key] = item = undefined; 45 | } 46 | } 47 | return item ? item.value : undefined; 48 | } 49 | } 50 | 51 | module.exports = Cache; 52 | -------------------------------------------------------------------------------- /lib/config.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const fs = require('fs'); 4 | const Promise = require('bluebird'); 5 | const Utils = require('./utils'); 6 | 7 | /** 8 | * @name Config 9 | * @summary config helper 10 | * @return {undefined} 11 | */ 12 | class Config { 13 | /** 14 | * @name constructor 15 | * @summary init config object 16 | * @return {undefined} 17 | */ 18 | constructor() { 19 | this.config = {}; 20 | } 21 | 22 | /** 23 | * @name getObject 24 | * @summary Returns a plain-old JavaScript object 25 | * @return {object} obj - a Plain old JavaScript Object. 26 | */ 27 | getObject() { 28 | return Object.assign({}, this.config); 29 | } 30 | 31 | /** 32 | * @name _doInit 33 | * @summary Perform initialization process. 34 | * @param {string/object} cfg - path or URL to configuration JSON data or object 35 | * @param {function} resolve - resolve function 36 | * @param {function} reject - reject function 37 | * @return {undefined} 38 | */ 39 | _doInit(cfg, resolve, reject) { 40 | if (!cfg) { 41 | reject(new Error('no config specified')); 42 | } else { 43 | try { 44 | if (typeof cfg === 'string') { 45 | this._doInitViaFile(cfg, resolve, reject); 46 | } else { 47 | this.config = Object.assign({}, cfg); 48 | resolve(); 49 | } 50 | } catch (err) { 51 | reject(err); 52 | } 53 | } 54 | } 55 | 56 | /** 57 | * @name _doInitViaFile 58 | * @summary Perform initialization from a file. 59 | * @param {string} configFilePath - path to configuration JSON data 60 | * @param {function} resolve - resolve function 61 | * @param {function} reject - reject function 62 | * @return {undefined} 63 | */ 64 | _doInitViaFile(configFilePath, resolve, reject) { 65 | fs.readFile(configFilePath, (err, result) => { 66 | if (!err) { 67 | let config = Utils.safeJSONParse(result.toString()); 68 | if (!config) { 69 | reject(new Error('unable to parse config file')); 70 | return; 71 | } 72 | if (config.location) { 73 | this._doInit(config.location, resolve, reject); 74 | } else { 75 | this.config = Object.assign({}, config); 76 | resolve(); 77 | } 78 | } else { 79 | reject(err); 80 | } 81 | }); 82 | } 83 | 84 | /** 85 | * @name init 86 | * @summary Initializes config object with JSON file data. 87 | * @param {object/string} cfg - path to config file or config object 88 | * @return {object} promise - resolves if successful, else rejects 89 | */ 90 | init(cfg) { 91 | return new Promise((resolve, reject) => { 92 | this._doInit(cfg, resolve, reject); 93 | }); 94 | } 95 | } 96 | 97 | /** 98 | * Return an ES6 Proxy object which provides access to configuration fields. 99 | */ 100 | module.exports = new Proxy(new Config(), { 101 | get: function(target, name, _receiver) { 102 | return name in target ? 103 | target[name] : target.config[name]; 104 | } 105 | }); 106 | -------------------------------------------------------------------------------- /lib/redis-connection.js: -------------------------------------------------------------------------------- 1 | const Promise = require('bluebird'); 2 | let redis; 3 | 4 | /** 5 | * @name RedisConnection 6 | * @summary Handles Redis connect 7 | */ 8 | class RedisConnection { 9 | /** 10 | * @name constructor 11 | * @summary 12 | * @description 13 | * @param {object} redisConfig - redis configuration object 14 | * @param {number} defaultRedisDb - default redis database number 15 | * @param {boolean} testMode - whether redis mock library is being used 16 | * @return {undefined} 17 | */ 18 | constructor(redisConfig, defaultRedisDb = 0, testMode = false) { 19 | if (testMode) { 20 | redis = require('redis-mock'); 21 | } else { 22 | redis = require('redis'); 23 | Promise.promisifyAll(redis.RedisClient.prototype); 24 | Promise.promisifyAll(redis.Multi.prototype); 25 | } 26 | 27 | let url = {}; 28 | if (redisConfig.url) { 29 | let parsedUrl = require('redis-url').parse(redisConfig.url); 30 | url = { 31 | host: parsedUrl.hostname, 32 | port: parsedUrl.port, 33 | db: parsedUrl.database 34 | }; 35 | if (parsedUrl.password) { 36 | url.password = parsedUrl.password; 37 | } 38 | } 39 | this.redisConfig = Object.assign({db: defaultRedisDb}, url, redisConfig); 40 | if (this.redisConfig.host) { 41 | delete this.redisConfig.url; 42 | } 43 | this.options = { 44 | maxReconnectionAttempts: 5, 45 | maxDelayBetweenReconnections: 2 46 | }; 47 | } 48 | 49 | /** 50 | * @name getRedis 51 | * @summary Get Redis constructor 52 | * @return {funcion} redis 53 | */ 54 | getRedis() { 55 | return redis; 56 | } 57 | 58 | /** 59 | * @name connect 60 | * @summary connection entry point 61 | * @param {object} options - connection options - description 62 | * @return {undefined} 63 | */ 64 | connect(options) { 65 | if (options) { 66 | this.options = options; 67 | } 68 | let reconnections = 0; 69 | let executor = this.attempt(() => this._connect()); 70 | return executor.until( 71 | 'Max reconnection attempts reached. Check connection to Redis.', 72 | () => { 73 | if (reconnections > 0) { 74 | console.log(`Redis reconnection attempt ${reconnections + 1} of ${this.options.maxReconnectionAttempts}...`); 75 | } 76 | return ++reconnections >= this.options.maxReconnectionAttempts; 77 | } 78 | ); 79 | } 80 | 81 | /** 82 | * @name _connect 83 | * @summary private member used by connect to rettry connections 84 | * @return {object} promise 85 | */ 86 | _connect() { 87 | let self = new Promise((resolve, reject) => { 88 | let db = redis.createClient(this.redisConfig); 89 | db.once('ready', () => resolve(db)); 90 | db.on('error', (err) => { 91 | if (self.isPending()) { 92 | return reject(err); 93 | } 94 | }); 95 | }); 96 | return self; 97 | } 98 | 99 | /** 100 | * @name attempt 101 | * @summary connection attempt 102 | * @param {function} action 103 | * @return {undefined} 104 | */ 105 | attempt(action) { 106 | let self = { 107 | until: (rejection, condition) => new Promise((resolve, reject) => { 108 | if (condition()) { 109 | reject(new Error(rejection)); 110 | } else { 111 | action() 112 | .then(resolve) 113 | .catch(() => { 114 | setTimeout(() => { 115 | resolve(self.until(rejection, condition)); 116 | }, this.options.maxDelayBetweenReconnections * 1000); 117 | }); 118 | } 119 | }) 120 | }; 121 | return self; 122 | } 123 | } 124 | 125 | module.exports = RedisConnection; 126 | -------------------------------------------------------------------------------- /lib/server-request.js: -------------------------------------------------------------------------------- 1 | const http = require('http'); 2 | const REQUEST_TIMEOUT = 30000; // 30-seconds 3 | 4 | /** 5 | * @name ServerRequest 6 | * @summary Class for handling server requests 7 | */ 8 | class ServerRequest { 9 | /** 10 | * @name constructor 11 | * @summary Class constructor 12 | * @return {undefined} 13 | */ 14 | constructor() { 15 | } 16 | 17 | /** 18 | * @name send 19 | * @summary sends an HTTP Request 20 | * @param {object} options - request options 21 | * @return {object} promise 22 | */ 23 | send(options) { 24 | return new Promise((resolve, reject) => { 25 | if (options.method === 'POST' || options.method === 'PUT' || options.method === 'PATCH') { 26 | options.headers = options.headers || {}; 27 | options.headers['content-length'] = Buffer.byteLength(options.body, 'utf8'); 28 | } else { 29 | delete options.body; 30 | } 31 | 32 | let req = http.request(options, (res) => { 33 | let response = []; 34 | res.on('data', (data) => { 35 | response.push(data); 36 | }); 37 | res.on('end', () => { 38 | let buffer = Buffer.concat(response); 39 | let data = { 40 | statusCode: res.statusCode, 41 | headers: res.headers 42 | }; 43 | data.headers['content-length'] = Buffer.byteLength(buffer); 44 | data.payLoad = buffer; 45 | resolve(data); 46 | }); 47 | res.on('error', (err) => { 48 | reject(err); 49 | }); 50 | }); 51 | 52 | req.on('socket', (socket) => { 53 | socket.setNoDelay(true); 54 | socket.setTimeout(options.timeout * 1000 || REQUEST_TIMEOUT, () => { 55 | req.abort(); 56 | }); 57 | }); 58 | 59 | req.on('error', (err) => { 60 | reject(err); 61 | }); 62 | 63 | if (options.body) { 64 | req.write(options.body); 65 | } 66 | req.end(); 67 | }); 68 | } 69 | } 70 | 71 | module.exports = ServerRequest; 72 | -------------------------------------------------------------------------------- /lib/server-response.js: -------------------------------------------------------------------------------- 1 | /** 2 | * @name ServerResponse 3 | * @summary Class for handling server responses 4 | */ 5 | class ServerResponse { 6 | /** 7 | * @name constructor 8 | * @summary Class constructor 9 | * @return {undefined} 10 | */ 11 | constructor() { 12 | this.testMode = false; 13 | this.corsEnabled = false; 14 | this.corsHeaders = { 15 | 'access-control-allow-origin': '*' 16 | }; 17 | } 18 | 19 | /** 20 | * @name setTestMode 21 | * @summary Set this module in test mode 22 | * @return {undefined} 23 | */ 24 | setTestMode() { 25 | this.testMode = true; 26 | } 27 | 28 | /** 29 | * @name enableCORS 30 | * @summary Enable / Disable CORS support 31 | * @param {boolean} state - true if CORS should be enabled 32 | * @param {object} corsHeaders - request return cors headers 33 | * @return {undefined} 34 | */ 35 | enableCORS(state, corsHeaders) { 36 | this.corsEnabled = state; 37 | if (corsHeaders) { 38 | this.corsHeaders = Object.assign(this.corsHeaders, corsHeaders); 39 | } 40 | } 41 | 42 | /** 43 | * @name createResponseObject 44 | * @summary Create a data response object. 45 | * @description This creates a consistently formatted HTTP response. It can be used 46 | * with any of the server-response send methods in the data param. 47 | * @param {number} httpCode - HTTP code (Ex. 404) 48 | * @param {object} resultPayload - object with {result: somevalue} 49 | * @return {object} response - object suitable for sending via HTTP 50 | */ 51 | createResponseObject(httpCode, resultPayload) { 52 | let status = ServerResponse.STATUS[httpCode] || [`${httpCode}`, `Unknown statusCode ${httpCode}`]; 53 | let response = Object.assign({ 54 | statusCode: httpCode, 55 | statusMessage: status[ServerResponse.STATUSMESSAGE], 56 | statusDescription: status[ServerResponse.STATUSDESCRIPTION], 57 | result: {} 58 | }, resultPayload || {}); 59 | return response; 60 | } 61 | 62 | /** 63 | * @name sendResponse 64 | * @summary Send a server response to caller. 65 | * @param {number} code - HTTP response code 66 | * @param {object} res - Node HTTP response object 67 | * @param {object} data - An object to send 68 | * @return {object} res - Returns the (res) response object when in test mode, else undefined 69 | */ 70 | sendResponse(code, res, data) { 71 | let headers = { 72 | 'Content-Type': 'application/json' 73 | }; 74 | 75 | let response = Object.assign(this.createResponseObject(code), data || {}); 76 | if (this.corsEnabled) { 77 | headers = Object.assign(headers, this.corsHeaders); 78 | } 79 | 80 | if (data) { 81 | headers = Object.assign(headers, data.headers); 82 | } 83 | 84 | let responseString = JSON.stringify(response); 85 | headers['Content-Length'] = Buffer.byteLength(responseString); 86 | 87 | res.writeHead(code, headers); 88 | res.write(responseString); 89 | res.end(); 90 | } 91 | 92 | /** 93 | * @name sendOk 94 | * @summary Send an HTTP_OK server response to caller. 95 | * @param {object} res - Node HTTP response object 96 | * @param {object} data - An object to send 97 | * @return {undefined} 98 | */ 99 | sendOk(res, data) { 100 | this.sendResponse(ServerResponse.HTTP_OK, res, data); 101 | } 102 | 103 | /** 104 | * @name sendCreated 105 | * @summary Send an HTTP_CREATED server response to caller. 106 | * @param {object} res - Node HTTP response object 107 | * @param {object} data - An object to send 108 | * @return {undefined} 109 | */ 110 | sendCreated(res, data) { 111 | this.sendResponse(ServerResponse.HTTP_CREATED, res, data); 112 | } 113 | 114 | /** 115 | * @name sendNoContent 116 | * @summary Send an HTTP_NO_CONTENT server response to caller. 117 | * @param {object} res - Node HTTP response object 118 | * @param {object} data - An object to send 119 | * @return {undefined} 120 | */ 121 | sendNoContent(res, data) { 122 | this.sendResponse(ServerResponse.HTTP_NO_CONTENT, res, data); 123 | } 124 | 125 | /** 126 | * @name sendMovedPermanently 127 | * @summary Send an HTTP_MOVED_PERMANENTLY server response to caller. 128 | * @param {object} res - Node HTTP response object 129 | * @param {object} data - An object to send 130 | * @return {undefined} 131 | */ 132 | sendMovedPermanently(res, data) { 133 | this.sendResponse(ServerResponse.HTTP_MOVED_PERMANENTLY, res, data); 134 | } 135 | 136 | /** 137 | * @name sendInvalidRequest 138 | * @summary Send an HTTP_BAD_REQUEST server response to caller. 139 | * @param {object} res - Node HTTP response object 140 | * @param {object} data - An object to send 141 | * @return {undefined} 142 | */ 143 | sendInvalidRequest(res, data) { 144 | this.sendResponse(ServerResponse.HTTP_BAD_REQUEST, res, data); 145 | } 146 | 147 | /** 148 | * @name sendInvalidUserCredentials 149 | * @summary Send an HTTP_UNAUTHORIZED server response to caller. 150 | * @param {object} res - Node HTTP response object 151 | * @param {object} data - An object to send 152 | * @return {undefined} 153 | */ 154 | sendInvalidUserCredentials(res, data) { 155 | this.sendResponse(ServerResponse.HTTP_UNAUTHORIZED, res, data); 156 | } 157 | 158 | /** 159 | * @name sendPaymentRequired 160 | * @summary Send an HTTP_PAYMENT_REQUIRED server response to caller. 161 | * @param {object} res - Node HTTP response object 162 | * @param {object} data - An object to send 163 | * @return {undefined} 164 | */ 165 | sendPaymentRequired(res, data) { 166 | this.sendResponse(ServerResponse.HTTP_PAYMENT_REQUIRED, res, data); 167 | } 168 | 169 | /** 170 | * @name sendForbidden 171 | * @summary Send an HTTP_FORBIDDEN server response to caller. 172 | * @param {object} res - Node HTTP response object 173 | * @param {object} data - An object to send 174 | * @return {undefined} 175 | */ 176 | sendForbidden(res, data) { 177 | this.sendResponse(ServerResponse.HTTP_FORBIDDEN, res, data); 178 | } 179 | 180 | /** 181 | * @name sendNotFound 182 | * @summary Send an HTTP_NOT_FOUND server response to caller. 183 | * @param {object} res - Node HTTP response object 184 | * @param {object} data - An object to send 185 | * @return {undefined} 186 | */ 187 | sendNotFound(res, data) { 188 | this.sendResponse(ServerResponse.HTTP_NOT_FOUND, res, data); 189 | } 190 | 191 | /** 192 | * @name sendInvalidSession 193 | * @summary Send an HTTP_BAD_REQUEST server response to caller. 194 | * @param {object} res - Node HTTP response object 195 | * @param {object} data - An object to send 196 | * @return {undefined} 197 | */ 198 | sendInvalidSession(res, data) { 199 | this.sendResponse(ServerResponse.HTTP_BAD_REQUEST, res, data); 200 | } 201 | 202 | /** 203 | * @name sendRequestFailed 204 | * @summary Send an HTTP_SERVER_ERROR server response to caller. 205 | * @param {object} res - Node HTTP response object 206 | * @param {object} data - An object to send 207 | * @return {undefined} 208 | */ 209 | sendRequestFailed(res, data) { 210 | this.sendResponse(ServerResponse.HTTP_SERVER_ERROR, res, data); 211 | } 212 | 213 | /** 214 | * @name sendDataConflict 215 | * @summary Send an HTTP_CONFLICT server response to caller. 216 | * @param {object} res - Node HTTP response object 217 | * @param {object} data - An object to send 218 | * @return {undefined} 219 | */ 220 | sendDataConflict(res, data) { 221 | this.sendResponse(ServerResponse.HTTP_CONFLICT, res, data); 222 | } 223 | 224 | /** 225 | * @name sendTooLarge 226 | * @summary Send an HTTP_TOO_LARGE server response to caller. 227 | * @param {object} res - Node HTTP response object 228 | * @param {object} data - An object to send 229 | * @return {undefined} 230 | */ 231 | sendTooLarge(res, data) { 232 | this.sendResponse(ServerResponse.HTTP_TOO_LARGE, res, data); 233 | } 234 | 235 | /** 236 | * @name sendTooManyRequests 237 | * @summary Send an HTTP_TOO_MANY_REQUEST server response to caller. 238 | * @param {object} res - Node HTTP response object 239 | * @param {object} data - An object to send 240 | * @return {undefined} 241 | */ 242 | sendTooManyRequests(res, data) { 243 | this.sendResponse(ServerResponse.HTTP_TOO_MANY_REQUEST, res, data); 244 | } 245 | 246 | /** 247 | * @name sendServerError 248 | * @summary Send an HTTP_SERVER_ERROR server response to caller. 249 | * @param {object} res - Node HTTP response object 250 | * @param {object} data - An object to send 251 | * @return {undefined} 252 | */ 253 | sendServerError(res, data) { 254 | this.sendResponse(ServerResponse.HTTP_SERVER_ERROR, res, data); 255 | } 256 | 257 | /** 258 | * @name sendInternalError 259 | * @summary Alias for sendServerError 260 | * @param {object} res - Node HTTP response object 261 | * @param {object} data - An object to send 262 | * @return {undefined} 263 | */ 264 | sendInternalError(res, data) { 265 | this.sendServerError(res, data); 266 | } 267 | 268 | /** 269 | * @name sendMethodNotImplemented 270 | * @summary Send an HTTP_METHOD_NOT_IMPLEMENTED server response to caller. 271 | * @param {object} res - Node HTTP response object 272 | * @param {object} data - An object to send 273 | * @return {undefined} 274 | */ 275 | sendMethodNotImplemented(res, data) { 276 | this.sendResponse(ServerResponse.HTTP_METHOD_NOT_IMPLEMENTED, res, data); 277 | } 278 | 279 | /** 280 | * @name sendConnectionRefused 281 | * @summary Send an HTTP_CONNECTION_REFUSED server response to caller. 282 | * @param {object} res - Node HTTP response object 283 | * @param {object} data - An object to send 284 | * @return {undefined} 285 | */ 286 | sendUnavailableError(res, data) { 287 | this.sendResponse(ServerResponse.HTTP_CONNECTION_REFUSED, res, data); 288 | } 289 | } 290 | 291 | /** 292 | * Response codes. 293 | */ 294 | ServerResponse.HTTP_OK = 200; 295 | ServerResponse.HTTP_CREATED = 201; 296 | ServerResponse.HTTP_ACCEPTED = 202; 297 | ServerResponse.HTTP_NO_CONTENT = 204; 298 | ServerResponse.HTTP_MOVED_PERMANENTLY = 301; 299 | ServerResponse.HTTP_FOUND = 302; 300 | ServerResponse.HTTP_NOT_MODIFIED = 304; 301 | ServerResponse.HTTP_BAD_REQUEST = 400; 302 | ServerResponse.HTTP_UNAUTHORIZED = 401; 303 | ServerResponse.HTTP_PAYMENT_REQUIRED = 402; 304 | ServerResponse.HTTP_FORBIDDEN = 403; 305 | ServerResponse.HTTP_NOT_FOUND = 404; 306 | ServerResponse.HTTP_METHOD_NOT_ALLOWED = 405; 307 | ServerResponse.NOT_ACCEPTABLE = 406; 308 | ServerResponse.HTTP_CONFLICT = 409; 309 | ServerResponse.HTTP_TOO_LARGE = 413; 310 | ServerResponse.HTTP_TOO_MANY_REQUEST = 429; 311 | ServerResponse.HTTP_SERVER_ERROR = 500; 312 | ServerResponse.HTTP_METHOD_NOT_IMPLEMENTED = 501; 313 | ServerResponse.HTTP_CONNECTION_REFUSED = 502; 314 | ServerResponse.HTTP_SERVICE_UNAVAILABLE = 503; 315 | 316 | ServerResponse.STATUSMESSAGE = 0; 317 | ServerResponse.STATUSDESCRIPTION = 1; 318 | ServerResponse.STATUS = { 319 | '200': ['OK', 'Request succeeded without error'], 320 | '201': ['Created', 'Resource created'], 321 | '202': ['Accepted', 'Request accepted'], 322 | '204': ['No Content', 'Resource has no content'], 323 | '301': ['Moved Permanently', 'Resource has been permanently moved'], 324 | '302': ['Found', 'Resource was found under another URI'], 325 | '304': ['Not Modified', 'Resource has not been modified'], 326 | '400': ['Bad Request', 'Request is invalid, missing parameters?'], 327 | '401': ['Unauthorized', 'User isn\'t authorized to access this resource'], 328 | '402': ['Payment Required', 'This code is reserved for future use.'], 329 | '403': ['Forbidden', 'The server understood the request but refuses to authorize it'], 330 | '404': ['Not Found', 'The requested resource was not found on the server'], 331 | '405': ['Method not allowed', 'The HTTP method used is not allowed'], 332 | '406': ['Not Acceptable', 'The target resource does not have a current representation that would be acceptable to the user agent'], 333 | '409': ['Conflict', 'Request has caused a conflict'], 334 | '413': ['Request Entity Too Large', 'The webserver or proxy believes the request is too large'], 335 | '429': ['Too Many Requests', 'Too many requests issue within a period'], 336 | '500': ['Server Error', 'An error occurred on the server'], 337 | '501': ['Method Not Implemented', 'The requested method / resource isn\'t implemented on the server'], 338 | '502': ['Connection Refused', 'The connection to server was refused'], 339 | '503': ['Service Unavailable', 'The server is currently unable to handle the request due to a temporary overloading or maintenance of the server'] 340 | }; 341 | 342 | module.exports = ServerResponse; 343 | -------------------------------------------------------------------------------- /lib/umfmessage.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const uuid = require('uuid'); 4 | const crypto = require('crypto'); 5 | const UMF_VERSION = 'UMF/1.4.6'; 6 | 7 | /** 8 | * @name UMFMessage 9 | * @summary UMF Message helper 10 | */ 11 | class UMFMessage { 12 | /** 13 | * @name constructor 14 | * @summary class constructor 15 | * @return {undefined} 16 | */ 17 | constructor() { 18 | this.message = {}; 19 | } 20 | 21 | /** 22 | * @name getTimeStamp 23 | * @summary retrieve an ISO 8601 timestamp 24 | * @return {string} timestamp - ISO 8601 timestamp 25 | */ 26 | getTimeStamp() { 27 | return new Date().toISOString(); 28 | } 29 | 30 | /** 31 | * @name createMessageID 32 | * @summary Returns a UUID for use with messages 33 | * @return {string} uuid - UUID 34 | */ 35 | createMessageID() { 36 | return uuid.v4(); 37 | } 38 | 39 | /** 40 | * @name createShortMessageID 41 | * @summary Returns a short form UUID for use with messages 42 | @see https://en.wikipedia.org/wiki/Base36 43 | * @return {string} short identifer 44 | */ 45 | createShortMessageID() { 46 | return (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(36); 47 | } 48 | 49 | /** 50 | * @name signMessage 51 | * @summary sign message with cryptographic signature 52 | * @param {string} algo - such as 'sha256' 53 | * @param {string} sharedSecret - shared secret 54 | * @return {undefined} 55 | */ 56 | signMessage(algo, sharedSecret) { 57 | (this.message.signature) && delete this.message.signature; 58 | this.message.signature = crypto 59 | .createHmac(algo, sharedSecret) 60 | .update(JSON.stringify(this.message)) 61 | .digest('hex'); 62 | } 63 | 64 | /** 65 | * @name toJSON 66 | * @return {object} A JSON stringifiable version of message 67 | */ 68 | toJSON() { 69 | return this.message; 70 | } 71 | 72 | /** 73 | * @name toShort 74 | * @summary convert a long message to a short one 75 | * @return {object} converted message 76 | */ 77 | toShort() { 78 | let message = {}; 79 | if (this.message['to']) { 80 | message['to'] = this.message['to']; 81 | } 82 | if (this.message['from']) { 83 | message['frm'] = this.message['from']; 84 | } 85 | if (this.message['headers']) { 86 | message['hdr'] = this.message['headers']; 87 | } 88 | if (this.message['mid']) { 89 | message['mid'] = this.message['mid']; 90 | } 91 | if (this.message['rmid']) { 92 | message['rmid'] = this.message['rmid']; 93 | } 94 | if (this.message['signature']) { 95 | message['sig'] = this.message['signature']; 96 | } 97 | if (this.message['timeout']) { 98 | message['tmo'] = this.message['timeout']; 99 | } 100 | if (this.message['timestamp']) { 101 | message['ts'] = this.message['timestamp']; 102 | } 103 | if (this.message['type']) { 104 | message['typ'] = this.message['type']; 105 | } 106 | if (this.message['version']) { 107 | message['ver'] = this.message['version']; 108 | } 109 | if (this.message['via']) { 110 | message['via'] = this.message['via']; 111 | } 112 | if (this.message['forward']) { 113 | message['fwd'] = this.message['forward']; 114 | } 115 | if (this.message['body']) { 116 | message['bdy'] = this.message['body']; 117 | } 118 | if (this.message['authorization']) { 119 | message['aut'] = this.message['authorization']; 120 | } 121 | return message; 122 | } 123 | 124 | /** 125 | * @name validate 126 | * @summary Validates that a UMF message has required fields 127 | * @return {boolean} response - returns true is valid otherwise false 128 | */ 129 | validate() { 130 | if (!this.message.from || !this.message.to || !this.message.body) { 131 | return false; 132 | } else { 133 | return true; 134 | } 135 | } 136 | } 137 | 138 | /** 139 | * @name createMessageInstance 140 | * @summary Create a message instance 141 | * @param {object} message - message object 142 | * @return {undefined} 143 | */ 144 | function createMessageInstance(message) { 145 | let proxy = new Proxy(new UMFMessage(), { 146 | get: function(target, name, _receiver) { 147 | return name in target ? 148 | target[name] : target.message[name]; 149 | }, 150 | set: (obj, prop, value) => { 151 | obj.message[prop] = value; 152 | return true; 153 | } 154 | }); 155 | if (message.to) { 156 | proxy.to = message.to; 157 | } 158 | if (message.from || message.frm) { 159 | proxy.from = message.from || message.frm; 160 | } 161 | if (message.headers || message.hdr) { 162 | proxy.headers = message.headers || message.hdr; 163 | } 164 | proxy.mid = message.mid || proxy.createMessageID(); 165 | if (message.rmid) { 166 | proxy.rmid = message.rmid; 167 | } 168 | if (message.signature || message.sig) { 169 | proxy.signature = message.signature || message.sig; 170 | } 171 | if (message.timeout || message.tmo) { 172 | proxy.timeout = message.timeout || message.tmo; 173 | } 174 | proxy.timestamp = message.timestamp || message.ts || proxy.getTimeStamp(); 175 | if (message.type || message.typ) { 176 | proxy.type = message.type || message.typ; 177 | } 178 | proxy.version = message.version || message.ver || UMF_VERSION; 179 | if (message.via) { 180 | proxy.via = message.via; 181 | } 182 | if (message.forward || message.fwd) { 183 | proxy.forward = message.forward || message.fwd; 184 | } 185 | if (message.body || message.bdy) { 186 | proxy.body = message.body || message.bdy; 187 | } 188 | if (message.authorization || message.aut) { 189 | proxy.authorization = message.authorization || message.aut; 190 | } 191 | return proxy; 192 | } 193 | 194 | /** 195 | * @name parseRoute 196 | * @summary parses message route strings 197 | * @private 198 | * @param {string} toValue - string to be parsed 199 | * @return {object} object - containing route parameters. If the 200 | * object contains an error field then the route 201 | * isn't valid. 202 | */ 203 | function parseRoute(toValue) { 204 | let serviceName = ''; 205 | let httpMethod; 206 | let apiRoute = ''; 207 | let error = ''; 208 | let urlRoute = toValue; 209 | let instance = ''; 210 | let subID = ''; 211 | 212 | let segments = urlRoute.split(':'); 213 | if (segments.length < 2) { 214 | error = 'route field has invalid number of routable segments'; 215 | } else { 216 | let atPos = segments[0].indexOf('@'); 217 | if (atPos > -1) { 218 | let x = segments.shift(); 219 | instance = x.substring(0, atPos); 220 | segments.unshift(x.substring(atPos + 1)); 221 | let segs = instance.split('-'); 222 | if (segs.length > 1) { 223 | instance = segs[0]; 224 | subID = segs[1]; 225 | } 226 | } 227 | if (segments[0].indexOf('http') === 0) { 228 | let url = `${segments[0]}:${segments[1]}`; 229 | segments.shift(); 230 | segments[0] = url; 231 | } 232 | serviceName = segments.shift(); 233 | apiRoute = segments.join(':'); 234 | let s1 = apiRoute.indexOf('['); 235 | if (s1 === 0) { 236 | let s2 = apiRoute.indexOf(']'); 237 | if (s2 < 0) { 238 | error = 'route field has ill-formed HTTP method verb in segment'; 239 | } else { 240 | httpMethod = apiRoute.substring(s1 + 1, s2).toLowerCase(); 241 | } 242 | if (!error) { 243 | let s3 = httpMethod.length; 244 | if (s3 > 0) { 245 | apiRoute = apiRoute.substring(s3 + 2, apiRoute.length); 246 | } 247 | } 248 | } 249 | } 250 | return { 251 | instance, 252 | subID, 253 | serviceName, 254 | httpMethod, 255 | apiRoute, 256 | error 257 | }; 258 | } 259 | 260 | /** 261 | * Return an ES6 Proxy object which provides access to message fields. 262 | */ 263 | module.exports = { 264 | createMessage: createMessageInstance, 265 | parseRoute: parseRoute 266 | }; 267 | -------------------------------------------------------------------------------- /lib/utils.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const crypto = require('crypto'); 4 | 5 | /** 6 | * @name Utils 7 | * @return {undefined} 8 | */ 9 | class Utils { 10 | /** 11 | * @name md5Hash 12 | * @summary Hashes a key to produce an MD5 hash 13 | * @param {string} key - input key to hash 14 | * @return {string} hash - hashed value 15 | */ 16 | static md5Hash(key) { 17 | return crypto 18 | .createHash('md5') 19 | .update(key) 20 | .digest('hex'); 21 | } 22 | 23 | /** 24 | * @name safeJSONStringify 25 | * @summary Safe JSON stringify 26 | * @param {object} obj - object to stringify 27 | * @return {string} string - stringified object. 28 | */ 29 | static safeJSONStringify(obj) { 30 | // replaceErrors below credited to Jonathan Lonowski via Stackoverflow: 31 | // https://stackoverflow.com/questions/18391212/is-it-not-possible-to-stringify-an-error-using-json-stringify 32 | let replaceErrors = (key, value) => { 33 | if (value instanceof Error) { 34 | let error = {}; 35 | Object.getOwnPropertyNames(value).forEach((key) => { 36 | error[key] = value[key]; 37 | }); 38 | return error; 39 | } 40 | return value; 41 | }; 42 | return JSON.stringify(obj, replaceErrors); 43 | } 44 | 45 | /** 46 | * @name safeJSONParse 47 | * @summary Safe JSON parse 48 | * @private 49 | * @param {string} str - string which will be parsed 50 | * @return {object} obj - parsed object 51 | * Returns undefined if string can't be parsed into an object 52 | */ 53 | static safeJSONParse(str) { 54 | let data; 55 | try { 56 | data = JSON.parse(str); 57 | } catch (e) { 58 | data = undefined; 59 | } 60 | return data; 61 | } 62 | 63 | /** 64 | * @name stringHash 65 | * @summary returns a hash value for a supplied string 66 | * @see https://github.com/darkskyapp/string-hash 67 | * @private 68 | * @param {object} str - string to hash 69 | * @return {number} hash - hash value 70 | */ 71 | static stringHash(str) { 72 | let hash = 5381; 73 | let i = str.length; 74 | while (i) { 75 | hash = (hash * 33) ^ str.charCodeAt(--i); 76 | } 77 | /* JavaScript does bitwise operations (like XOR, above) on 32-bit signed 78 | * integers. Since we want the results to be always positive, convert the 79 | * signed int to an unsigned by doing an unsigned bitshift. */ 80 | return hash >>> 0; 81 | } 82 | 83 | /** 84 | * @name shortID 85 | * @summary generate a random id composed of alphanumeric characters 86 | * @see https://en.wikipedia.org/wiki/Base36 87 | * @return {string} random string id 88 | */ 89 | static shortID() { 90 | return (Math.floor(Math.random() * Number.MAX_SAFE_INTEGER)).toString(36); 91 | } 92 | 93 | /** 94 | * @name isUUID4 95 | * @summary determine whether a string is a valid UUID 96 | * @param {string} str - possible UUID 97 | * @return {undefined} 98 | */ 99 | static isUUID4(str) { 100 | const uuidPattern = '^[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}$'; 101 | return (new RegExp(uuidPattern)).test(str); 102 | } 103 | 104 | /** 105 | * @name shuffeArray 106 | * @summary shuffle an array in place 107 | * @param {array} a - array elements may be numbers, string or objects. 108 | * @return {undefined} 109 | */ 110 | static shuffleArray(a) { 111 | for (let i = a.length; i; i--) { 112 | let j = Math.floor(Math.random() * i); 113 | [a[i - 1], a[j]] = [a[j], a[i - 1]]; 114 | } 115 | } 116 | } 117 | 118 | module.exports = Utils; 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hydra", 3 | "version": "1.9.3", 4 | "license": "MIT", 5 | "author": "Carlos Justiniano", 6 | "contributors": "https://github.com/pnxtech/hydra/graphs/contributors", 7 | "description": "Hydra is a NodeJS light-weight library for building distributed computing applications such as microservices", 8 | "keywords": [ 9 | "hydra", 10 | "hydra-core", 11 | "microservice" 12 | ], 13 | "main": "index.js", 14 | "analyze": false, 15 | "engines": { 16 | "node": ">=6.2.1" 17 | }, 18 | "scripts": { 19 | "test": "mocha specs --reporter spec", 20 | "coverage": "nyc --reporter=text ./node_modules/mocha/bin/_mocha specs --reporter spec" 21 | }, 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/pnxtech/hydra.git" 25 | }, 26 | "dependencies": { 27 | "bluebird": "3.5.1", 28 | "debug": "2.6.9", 29 | "redis": "2.8.0", 30 | "redis-url": "1.2.1", 31 | "route-parser": "0.0.5", 32 | "uuid": "3.2.1" 33 | }, 34 | "devDependencies": { 35 | "chai": "3.5.0", 36 | "eslint": "4.18.2", 37 | "eslint-config-google": "0.7.1", 38 | "eslint-plugin-mocha": "4.9.0", 39 | "mocha": "8.1.3", 40 | "nyc": "15.1.0", 41 | "redis-mock": "0.17.0" 42 | }, 43 | "nyc": { 44 | "exclude": [ 45 | "specs/*.js", 46 | "specs/helpers/*.js" 47 | ] 48 | }, 49 | "bin": { 50 | "hydra": "index.js" 51 | }, 52 | "bugs": { 53 | "url": "https://github.com/pnxtech/hydra/issues" 54 | }, 55 | "homepage": "https://github.com/pnxtech/hydra#readme" 56 | } 57 | -------------------------------------------------------------------------------- /plugin.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const HydraEvent = require('./events'); 4 | 5 | /** 6 | * @name HydraPlugin 7 | * @description Extend this for hydra plugins 8 | */ 9 | class HydraPlugin { 10 | /** 11 | * @param {string} pluginName - unique name for the plugin 12 | */ 13 | constructor(pluginName) { 14 | this.name = pluginName; 15 | } 16 | 17 | /** 18 | * @name setHydra 19 | * @param {object} hydra - hydra instance 20 | * @return {undefined} 21 | */ 22 | setHydra(hydra) { 23 | this.hydra = hydra; 24 | this.hydra.on(HydraEvent.CONFIG_UPDATE_EVENT, (config) => this.updateConfig(config)); 25 | } 26 | 27 | /** 28 | * @name setConfig 29 | * @param {object} hydraConfig - the hydra config 30 | * @return {undefined} 31 | */ 32 | setConfig(hydraConfig) { 33 | this.hydraConfig = hydraConfig; 34 | this.opts = (hydraConfig.plugins && hydraConfig.plugins[this.name]) || {}; 35 | } 36 | 37 | /** 38 | * @name updateConfig 39 | * @param {object} serviceConfig - the service-level config 40 | * @param {object} serviceConfig.hydra - the hydra-level config 41 | * @return {undefined} 42 | */ 43 | updateConfig(serviceConfig) { 44 | this.serviceConfig = serviceConfig; 45 | this.hydraConfig = serviceConfig.hydra; 46 | let opts = (this.hydraConfig.plugins && this.hydraConfig.plugins[this.name]) || {}; 47 | let isEqual = (JSON.stringify(this.opts) == JSON.stringify(opts)); 48 | if (!isEqual) { 49 | this.configChanged(opts); 50 | } 51 | } 52 | 53 | /** 54 | * @name configChanged 55 | * @summary Handles changes to the plugin configuration 56 | * @param {object} opts - the new plugin config 57 | * @return {undefined} 58 | */ 59 | configChanged(opts) { 60 | console.log(`[override] [${this.name}] handle changed config`); 61 | console.dir(opts, {colors: true, depth: null}); 62 | } 63 | 64 | /** 65 | * @name onServiceReady 66 | * @summary Called by Hydra when the service has initialized, but before the init Promise resolves 67 | * @return {undefined} 68 | */ 69 | onServiceReady() { 70 | console.log(`[override] [${this.name}] hydra service ready`); 71 | } 72 | } 73 | 74 | module.exports = HydraPlugin; 75 | -------------------------------------------------------------------------------- /plugins.md: -------------------------------------------------------------------------------- 1 | # Hydra plugins 2 | 3 | Hydra's behavior and features can be extended through plugins, allowing different Hydra services or plugins to easily take advantage of shared functionalities. 4 | 5 | ## Overview 6 | 7 | Hydra plugins extend the HydraPlugin class. 8 | 9 | Plugins should be registered before Hydra is initialized via hydra.init. 10 | 11 | E.g. 12 | 13 | ```js 14 | const YourPlugin = require('./your-plugin'); 15 | hydra.use(new YourPlugin()); 16 | ``` 17 | 18 | Hydra will automatically call several hooks defined by the plugin class: 19 | 20 | | Hook | Description 21 | | --- | --- 22 | | `setHydra(hydra)` | called during plugin registration 23 | | `setConfig(config)` | called before hydra initialization 24 | | `updateConfig(serviceConfig)` | when the service-level config changes, will call configChanged if this plugin's options have changed 25 | | `configChanged(options)` | when the plugin-level options change 26 | | `onServiceReady()` | when the service has initialized but before the hydra.init Promise resolves 27 | 28 | ### Hook return values 29 | 30 | `setHydra`, `setConfig`, and `onServiceReady` can return a value or a Promise. 31 | 32 | The actual return value isn't important; if the hook returns a value, success is assumed. 33 | If an error in plugin initialization should result in the service failing to start, 34 | the plugin hook should throw an Error. 35 | 36 | Similarly if a Promise is returned and resolves, success is assumed; the resolve() value is ignored. 37 | Fatal errors should reject(). 38 | 39 | ## Quick Tutorial 40 | 41 | Set up a plugin in five easy steps. 42 | 43 | ### 1. Set up a hydra service: 44 | 45 | ``` 46 | $ yo fwsp-hydra 47 | ? Name of the service (`-service` will be appended automatically) pingpong 48 | ? Host the service runs on? 49 | ? Port the service runs on? 0 50 | ? What does this service do? demo 51 | ? Does this service need auth? No 52 | ? Is this a hydra-express service? No 53 | ? Set up logging? No 54 | ? Run npm install? Yes 55 | 56 | $ cd pingpong-service 57 | ``` 58 | 59 | ### 2. Create pong-plugin.js: 60 | 61 | ***Tip:** On OS X, you can copy this snippet and then `pbpaste > pong-plugin.js`* 62 | 63 | ```js 64 | // whenever a 'ping' event is emitted, a 'pong' event is emitted after a user-defined delay 65 | const Promise = require('bluebird'); 66 | const HydraPlugin = require('hydra/plugin'); 67 | 68 | class PongPlugin extends HydraPlugin { 69 | constructor() { 70 | super('example'); // unique identifier for the plugin 71 | } 72 | 73 | // called at the beginning of hydra.init 74 | // the parent class will locate the plugin config and set this.opts 75 | // can return a Promise or a value 76 | // in this case, there's no return statement, so that value is undefined 77 | setConfig(hydraConfig) { 78 | super.setConfig(hydraConfig); 79 | this.configChanged(this.opts); 80 | this.hydra.on('ping', () => { 81 | Promise.delay(this.opts.pongDelay).then(() => { 82 | this.hydra.emit('pong'); 83 | }); 84 | }) 85 | } 86 | 87 | // called when the config for this plugin has changed (via HydraEvent.CONFIG_UPDATE_EVENT) 88 | // if you need access to the full service config, override updateConfig(serviceConfig) 89 | configChanged(opts) { 90 | if (this.opts.pongDelay === "random") { 91 | this.opts.pongDelay = Math.floor(Math.random() * 3000); 92 | console.log(`Random delay = ${this.opts.pongDelay}`); 93 | } 94 | } 95 | 96 | // called after hydra has initialized but before hydra.init Promise resolves 97 | // can return a Promise or a value 98 | // this will delay by the port number in ms for demonstration of Promise 99 | onServiceReady() { 100 | console.log(`[example plugin] hydra service running on ${this.hydra.config.servicePort}`); 101 | console.log('[example plugin] delaying serviceReady...'); 102 | return new Promise((resolve, reject) => { 103 | Promise.delay(this.hydra.config.servicePort) 104 | .then(() => { 105 | console.log('[example plugin] delayed serviceReady, pinging...'); 106 | this.hydra.emit('ping'); 107 | resolve(); 108 | }); 109 | }); 110 | } 111 | } 112 | 113 | module.exports = PongPlugin; 114 | ``` 115 | 116 | ### 3. Update `hydra` section of `config.json` to pass the plugin configuration: 117 | 118 | ```json 119 | { 120 | "hydra": { 121 | "plugins": { 122 | "example": { 123 | "pongDelay": 2000 124 | } 125 | } 126 | } 127 | } 128 | ``` 129 | 130 | ### 4. Set up hydra service entry-point script: 131 | ```js 132 | const version = require('./package.json').version; 133 | const hydra = require('fwsp-hydra'); 134 | 135 | // install plugin 136 | const PongPlugin = require('./pong-plugin'); 137 | hydra.use(new PongPlugin()); 138 | 139 | // add some console.logs so we can see the events happen 140 | hydra.on('ping', () => console.log('PING!')); 141 | hydra.on('pong', () => { 142 | console.log('PONG!'); 143 | // send a ping back, after a random delay of up to 2s, to keep the rally going 144 | setTimeout(() => hydra.emit('ping'), Math.floor(Math.random() * 2000)); 145 | }); 146 | 147 | 148 | let config = require('fwsp-config'); 149 | config.init('./config/config.json') 150 | .then(() => { 151 | config.version = version; 152 | config.hydra.serviceVersion = version; 153 | hydra.init(config.hydra) 154 | .then(() => hydra.registerService()) 155 | .then(serviceInfo => { 156 | 157 | console.log(serviceInfo); // so we see when the serviceInfo resolves 158 | 159 | let logEntry = `Starting ${config.hydra.serviceName} (v.${config.version})`; 160 | hydra.sendToHealthLog('info', logEntry); 161 | }) 162 | .catch(err => { 163 | console.log('Error initializing hydra', err); 164 | }); 165 | }); 166 | ``` 167 | 168 | ### 5. Try it out! 169 | 170 | Run `npm start`. After an initial delay, you should start seeing PING!s and PONG!s. 171 | 172 | ### 6. Learn more from others: 173 | You may want to also learn from the implementation of the following hydra plugins as a reference: 174 | * [hydra-plugin-http](https://github.com/jkyberneees/hydra-plugin-http): Hydra plugin that adds traditional HTTP requests, routing and proxy capabilities to your hydra micro-services. 175 | * [hydra-plugin-rpc](https://github.com/ecwyne/hydra-plugin-rpc): Hydra-RPC Plugin for Hydra microservices library https://www.hydramicroservice.com 176 | -------------------------------------------------------------------------------- /specs/cache.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-invalid-this: 0 */ 2 | 3 | require('./helpers/chai.js'); 4 | 5 | const Cache = require('../lib/cache'); 6 | const version = require('../package.json').version; 7 | const SECOND = 1000; 8 | 9 | let cache; 10 | 11 | /** 12 | * @name Cache Tests 13 | * @summary Cache Test Suite 14 | */ 15 | describe('Cache', function() { 16 | this.timeout(SECOND * 10); 17 | 18 | beforeEach(() => { 19 | cache = new Cache(); 20 | }); 21 | 22 | afterEach((done) => { 23 | cache = null; 24 | done(); 25 | }); 26 | 27 | /** 28 | * @description Confirms that cache can put and get a value back within time 29 | */ 30 | it('should be able to add new value to cache and get it back', (done) => { 31 | cache.put('KEY5', 5, SECOND * 5); 32 | let val = cache.get('KEY5'); 33 | expect(val).to.equal(5); 34 | done(); 35 | }); 36 | 37 | /** 38 | * @description Confirms that cache will return undefined if not cached 39 | */ 40 | it('should return undefined if not cached before', (done) => { 41 | let val = cache.get('NO_SUCH_KEY'); 42 | expect(val).to.be.undefined; 43 | done(); 44 | }); 45 | 46 | /** 47 | * @description Confirms that cache will return undefined if expired 48 | */ 49 | it('should return undefined if cache expired', (done) => { 50 | cache.put('KEY6', 6); 51 | setTimeout(() => { 52 | let val = cache.get('KEY6'); 53 | expect(val).to.be.undefined; 54 | done(); 55 | }, SECOND * 2); 56 | }); 57 | }); -------------------------------------------------------------------------------- /specs/helpers/chai.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var chai = require('chai'); 4 | 5 | chai.config.includeStack = true; 6 | 7 | global.expect = chai.expect; 8 | global.AssertionError = chai.AssertionError; 9 | global.Assertion = chai.Assertion; 10 | global.assert = chai.assert; 11 | -------------------------------------------------------------------------------- /specs/index.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-invalid-this: 0 */ 2 | 3 | require('./helpers/chai.js'); 4 | 5 | let hydra; 6 | const version = require('../package.json').version; 7 | const redis = require('redis-mock'); 8 | const redisPort = 6379; 9 | const redisUrl = '127.0.0.1'; 10 | const SECOND = 1000; 11 | 12 | /** 13 | * @name getConfig 14 | * @summary Get a new copy of a config object 15 | * @return {undefined} 16 | */ 17 | function getConfig() { 18 | return Object.assign({}, { 19 | 'hydra': { 20 | 'serviceName': 'test-service', 21 | 'serviceDescription': 'Raison d\'etre', 22 | 'serviceIP': '127.0.0.1', 23 | 'servicePort': 5000, 24 | 'serviceType': 'test', 25 | 'redis': { 26 | 'url': redisUrl, 27 | 'port': redisPort, 28 | 'db': 0 29 | } 30 | }, 31 | version 32 | }); 33 | } 34 | 35 | /** 36 | * Change into specs folder so that config loading can find file using relative path. 37 | */ 38 | process.chdir('./specs'); 39 | 40 | /** 41 | * @name Index Tests 42 | * @summary Hydra Main Test Suite 43 | */ 44 | describe('Hydra Main', function() { 45 | this.timeout(SECOND * 10); 46 | 47 | beforeEach(() => { 48 | hydra = require('../index.js'); 49 | redis.removeAllListeners('message'); 50 | }); 51 | 52 | afterEach((done) => { 53 | hydra.shutdown().then(() => { 54 | let name = require.resolve('../index.js'); 55 | delete require.cache[name]; 56 | done(); 57 | }); 58 | }); 59 | 60 | /** 61 | * @description Confirms that hydra can connect to a redis instance 62 | */ 63 | it('should be able to connect to redis', (done) => { 64 | hydra.init(getConfig(), true) 65 | .then(() => { 66 | done(); 67 | }) 68 | .catch((_err) => { 69 | expect(true); 70 | }); 71 | }); 72 | 73 | /** 74 | * @description Hydra should fail on init() if called more than once 75 | */ 76 | it('should fail if init called more than once', (done) => { 77 | hydra.init(getConfig(), true) 78 | .then(() => { 79 | hydra.init(getConfig(), true) 80 | .then(() => { 81 | expect(true).to.be.false; 82 | done(); 83 | }) 84 | .catch((err) => { 85 | expect(err).to.not.be.null; 86 | expect(err.message).to.equal('Hydra.init() already invoked'); 87 | done(); 88 | }); 89 | }) 90 | .catch((_err) => { 91 | expect(true); 92 | done(); 93 | }); 94 | }); 95 | 96 | /** 97 | * @description Hydra should fail to load without a configuration file 98 | */ 99 | it('should fail without config file', (done) => { 100 | hydra.init({}, true) 101 | .then(() => { 102 | expect(true).to.be.false; 103 | done(); 104 | }) 105 | .catch((err) => { 106 | expect(err).to.not.be.null; 107 | expect(err.message).to.equal('Config missing serviceName or servicePort'); 108 | done(); 109 | }); 110 | }); 111 | 112 | /** 113 | * @description Hydra should load if serviceName and servicePort is provided 114 | */ 115 | it('should load if serviceName and servicePort is provided', (done) => { 116 | hydra.init({ 117 | hydra: { 118 | serviceName: 'test-service', 119 | servicePort: 3000 120 | } 121 | }, true) 122 | .then(() => { 123 | done(); 124 | }) 125 | .catch((err) => { 126 | expect(err).to.be.null; 127 | done(); 128 | }); 129 | }); 130 | 131 | /** 132 | * @description Hydra should load without a hydra.redis branch in configuration 133 | */ 134 | it('should load without config hydra.redis branch', (done) => { 135 | let config = getConfig(); 136 | delete config.hydra.redis; 137 | hydra.init(config, true) 138 | .then(() => { 139 | done(); 140 | }) 141 | .catch((err) => { 142 | expect(err).to.be.null; 143 | done(); 144 | }); 145 | }); 146 | 147 | /** 148 | * @description Hydra should fail if serviceName is missing in config 149 | */ 150 | it('should fail without serviceName config', (done) => { 151 | let config = getConfig(); 152 | delete config.hydra.serviceName; 153 | hydra.init(config, true) 154 | .then(() => { 155 | expect(true).to.be.false; 156 | done(); 157 | }) 158 | .catch((err) => { 159 | expect(err).to.not.be.null; 160 | expect(err.message).to.equal('Config missing serviceName or servicePort'); 161 | done(); 162 | }); 163 | }); 164 | 165 | /** 166 | * @description Confirms that when hydra registers as a service the expected keys can be found in redis 167 | */ 168 | it('should be able to register as a service', (done) => { 169 | hydra.init(getConfig(), true) 170 | .then(() => { 171 | let r = redis.createClient(); 172 | hydra.registerService() 173 | .then((_serviceInfo) => { 174 | setTimeout(() => { 175 | r.keys('*', (err, data) => { 176 | expect(err).to.be.null; 177 | expect(data.length).to.equal(3); 178 | expect(data).to.include('hydra:service:test-service:service'); 179 | expect(data).to.include('hydra:service:nodes'); 180 | done(); 181 | }); 182 | r.quit(); 183 | }, SECOND); 184 | }); 185 | }); 186 | }); 187 | 188 | /** 189 | * @description expect serviceName, serviceIP, servicePort and instanceID to exists upon service registration 190 | */ 191 | it('should have a serviceName, serviceIP, servicePort and instanceID', (done) => { 192 | hydra.init(getConfig(), true) 193 | .then(() => { 194 | hydra.registerService() 195 | .then((serviceInfo) => { 196 | expect(serviceInfo).not.null; 197 | expect(serviceInfo.serviceName).to.equal('test-service'); 198 | expect(serviceInfo.serviceIP).to.equal('127.0.0.1'); 199 | expect(serviceInfo.servicePort).to.equal('5000'); 200 | done(); 201 | }); 202 | }); 203 | }); 204 | 205 | /** 206 | * @description getServiceName should return name of service 207 | */ 208 | it('should see that getServiceName returns name of service', (done) => { 209 | hydra.init(getConfig(), true) 210 | .then(() => { 211 | hydra.registerService() 212 | .then((serviceInfo) => { 213 | expect(serviceInfo).not.null; 214 | expect(serviceInfo.serviceName).to.equal('test-service'); 215 | expect(hydra.getServiceName()).to.equal(serviceInfo.serviceName); 216 | done(); 217 | }); 218 | }); 219 | }); 220 | 221 | /** 222 | * @description getServices should return a list of services 223 | */ 224 | it('should see that getServices returns list of services', (done) => { 225 | hydra.init(getConfig(), true) 226 | .then(() => { 227 | hydra.registerService() 228 | .then((_serviceInfo) => { 229 | hydra.getServices() 230 | .then((services) => { 231 | expect(services.length).to.be.above(0); 232 | expect(services[0]).to.have.property('serviceName'); 233 | expect(services[0]).to.have.property('type'); 234 | expect(services[0]).to.have.property('registeredOn'); 235 | done(); 236 | }); 237 | }); 238 | }); 239 | }); 240 | 241 | /** 242 | * @description getServiceNodes should return a list of services 243 | */ 244 | it('should see that getServiceNodes returns list of service nodes', (done) => { 245 | hydra.init(getConfig(), true) 246 | .then(() => { 247 | hydra.registerService() 248 | .then((_serviceInfo) => { 249 | hydra.getServiceNodes() 250 | .then((nodes) => { 251 | expect(nodes.length).to.be.above(0); 252 | expect(nodes[0]).to.have.property('serviceName'); 253 | expect(nodes[0]).to.have.property('instanceID'); 254 | expect(nodes[0]).to.have.property('processID'); 255 | expect(nodes[0]).to.have.property('ip'); 256 | done(); 257 | }); 258 | }); 259 | }); 260 | }); 261 | 262 | /** 263 | * @description presence information should update in redis for a running hydra service 264 | */ 265 | it('should update presence', (done) => { 266 | hydra.init(getConfig(), true) 267 | .then(() => { 268 | let r = redis.createClient(); 269 | hydra.registerService() 270 | .then((_serviceInfo) => { 271 | let instanceID = hydra.getInstanceID(); 272 | r.hget('hydra:service:nodes', instanceID, (err, data) => { 273 | expect(err).to.be.null; 274 | expect(data).to.not.be.null; 275 | 276 | let entry = JSON.parse(data); 277 | setTimeout(() => { 278 | r.hget('hydra:service:nodes', instanceID, (err, data) => { 279 | expect(err).to.be.null; 280 | expect(data).to.not.be.null; 281 | let entry2 = JSON.parse(data); 282 | expect(entry2.updatedOn).to.not.equal(entry.updatedOn); 283 | r.quit(); 284 | done(); 285 | }); 286 | }, SECOND); 287 | }); 288 | }); 289 | }); 290 | }); 291 | 292 | /** 293 | * @description ensure keys expire on shutdown 294 | */ 295 | it('should expire redis keys on shutdown', (done) => { 296 | hydra.init(getConfig(), true) 297 | .then(() => { 298 | let r = redis.createClient(); 299 | hydra.registerService() 300 | .then((_serviceInfo) => { 301 | setTimeout(() => { 302 | r.get('hydra:service:test-service:73909f8c96a9d08e876411c0a212a1f4:presence', (err, _data) => { 303 | expect(err).to.be.null; 304 | done(); 305 | r.quit(); 306 | }); 307 | }, SECOND * 5); 308 | }); 309 | }); 310 | }); 311 | 312 | /** 313 | * @description service should be discoverable 314 | */ 315 | it('should be able to discover a service', (done) => { 316 | hydra.init(getConfig(), true) 317 | .then(() => { 318 | hydra.registerService() 319 | .then((_serviceInfo) => { 320 | setTimeout(() => { 321 | hydra.findService('test-service') 322 | .then((data) => { 323 | expect(data).not.null; 324 | expect(data.serviceName).to.equal('test-service'); 325 | expect(data.type).to.equal('test'); 326 | done(); 327 | }); 328 | }, SECOND); 329 | }); 330 | }); 331 | }); 332 | 333 | /** 334 | * @description invalid service should not be discoverable 335 | */ 336 | it('should return an error if a service doesn\'t exists', (done) => { 337 | hydra.init(getConfig(), true) 338 | .then(() => { 339 | hydra.registerService() 340 | .then((_serviceInfo) => { 341 | setTimeout(() => { 342 | hydra.findService('xyxyx-service') 343 | .then((_data) => { 344 | expect(true).to.be.false; 345 | done(); 346 | }) 347 | .catch((err) => { 348 | expect(err).to.not.be.null; 349 | expect(err.message).to.equal('Can\'t find xyxyx-service service'); 350 | done(); 351 | }); 352 | }, SECOND); 353 | }); 354 | }); 355 | }); 356 | 357 | /** 358 | * @description get service presence info 359 | */ 360 | it('should be able to retrieve service presence', (done) => { 361 | hydra.init(getConfig(), true) 362 | .then(() => { 363 | hydra.registerService() 364 | .then((_serviceInfo) => { 365 | hydra.getServicePresence('test-service') 366 | .then((data) => { 367 | expect(data).to.not.be.null; 368 | expect(data.length).to.be.above(0); 369 | expect(data[0]).to.have.property('processID'); 370 | expect(data[0].updatedOnTS).to.be.above(1492906823975); 371 | done(); 372 | }); 373 | }); 374 | }); 375 | }); 376 | }); 377 | 378 | /** 379 | * Change back to parent directory to maintain proper state 380 | */ 381 | process.chdir('..'); 382 | 383 | -------------------------------------------------------------------------------- /specs/umfmessage.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-invalid-this: 0 */ 2 | /* eslint semi: ["error", "always"] */ 3 | 4 | require('./helpers/chai.js'); 5 | 6 | const UMFMessage = require('../lib/umfmessage'); 7 | const SECOND = 1000; 8 | 9 | /** 10 | * @name UMFMessage Tests 11 | * @summary UMFMessage Test Suite 12 | */ 13 | describe('UMFMessage', function() { 14 | this.timeout(SECOND * 10); 15 | 16 | beforeEach(() => { 17 | }); 18 | 19 | afterEach((done) => { 20 | done(); 21 | }); 22 | 23 | /** 24 | * @description Get a valid UMF message from a base long message 25 | */ 26 | it('should instaniate a new UMF message from long form', (done) => { 27 | const inMessage = { 28 | 'to': 'xxx', 29 | 'from': 'yyy', 30 | 'headers': 'aaa', 31 | 'rmid': 'rmid123', 32 | 'signature': 'sjm', 33 | 'type': 'type-http', 34 | 'via': 'uid:123', 35 | 'timeout': 3000, 36 | 'forward': 'yyy@aaa', 37 | 'body': { 38 | 'a': 'a', 39 | 'b': 'b' 40 | }, 41 | 'authorization': 'secret' 42 | }; 43 | const umfMessage = UMFMessage.createMessage((inMessage)); 44 | expect(umfMessage).to.be.object; 45 | expect(umfMessage.validate()).to.be.true; 46 | done(); 47 | }); 48 | 49 | /** 50 | * @description Validate a bad message .. 51 | */ 52 | it('should fail validation of a bad message - no to', (done) => { 53 | const inMessage = { 54 | 'from': 'yyy', 55 | 'body': { 56 | 'a': 'a', 57 | 'b': 'b' 58 | } 59 | }; 60 | const umfMessage = UMFMessage.createMessage((inMessage)); 61 | const pass = umfMessage.validate(); 62 | expect(pass).to.be.false; 63 | done(); 64 | }); 65 | 66 | /** 67 | * @description Return JSON string of message .. 68 | */ 69 | it('should return valid JSON string', (done) => { 70 | const inMessage = { 71 | 'to': 'xxx', 72 | 'from': 'yyy', 73 | 'body': { 74 | 'a': 'a', 75 | 'b': 'b' 76 | } 77 | }; 78 | const umfMessage = UMFMessage.createMessage((inMessage)); 79 | const jsonMessage = umfMessage.toJSON(); 80 | expect(jsonMessage).to.be.string; 81 | expect(jsonMessage).to.be.defined; 82 | done(); 83 | }); 84 | 85 | /** 86 | * @description Transform long messsage to short 87 | */ 88 | it('should transform to a short message format', (done) => { 89 | const inMessage = { 90 | 'to': 'xxx', 91 | 'from': 'yyy', 92 | 'headers': 'aaa', 93 | 'rmid': 'rmid123', 94 | 'signature': 'sjm', 95 | 'type': 'type-http', 96 | 'via': 'uid:123', 97 | 'timeout': 3000, 98 | 'forward': 'yyy@aaa', 99 | 'body': { 100 | 'a': 'a', 101 | 'b': 'b' 102 | }, 103 | 'authorization': 'secret' 104 | }; 105 | 106 | const umfMessage = UMFMessage.createMessage((inMessage)); 107 | 108 | // Remove some fields to get coverage 109 | delete umfMessage.message.mid; 110 | delete umfMessage.message.timestamp; 111 | delete umfMessage.message.version; 112 | 113 | const shortMessage = umfMessage.toShort(); 114 | expect(shortMessage).to.be.object; 115 | done(); 116 | }); 117 | 118 | /** 119 | * @description Transform empty long messsage to short 120 | */ 121 | it('should transform empty message to a short message format', (done) => { 122 | const inMessage = { 123 | }; 124 | const umfMessage = UMFMessage.createMessage((inMessage)); 125 | const shortMessage = umfMessage.toShort(); 126 | expect(shortMessage).to.be.object; 127 | done(); 128 | }); 129 | 130 | /** 131 | * @description Transform long messsage to short 132 | */ 133 | it('should create valid message from short message format', (done) => { 134 | const inMessage = { 135 | 'to': 'xxx', 136 | 'frm': 'yyy', 137 | 'hdr': 'aaa', 138 | 'mid': 'mid123', 139 | 'rmid': 'rmid123', 140 | 'sig': 'sjm', 141 | 'typ': 'type-http', 142 | 'via': 'uid:123', 143 | 'tmo': 3000, 144 | 'ts': '2018-03-11T16:52:42.060Z', 145 | 'ver': '1.2', 146 | 'fwd': 'yyy@aaa', 147 | 'bdy': { 148 | 'a': 'a', 149 | 'b': 'b' 150 | }, 151 | 'aut': 'secret' 152 | }; 153 | const umfMessage = UMFMessage.createMessage((inMessage)); 154 | expect(umfMessage).to.be.object; 155 | done(); 156 | }); 157 | 158 | /** 159 | * @description Create a short message id 160 | */ 161 | it('should create short Message ID', (done) => { 162 | const inMessage = { 163 | 'to': 'xxx', 164 | 'from': 'yyy', 165 | 'headers': 'aaa', 166 | 'rmid': 'rmid123', 167 | 'signature': 'sjm', 168 | 'type': 'type-http', 169 | 'timestamp': '2018-03-11T16:52:42.060Z', 170 | 'version': '1.2', 171 | 'via': 'uid:123', 172 | 'timeout': 3000, 173 | 'forward': 'yyy@aaa', 174 | 'body': { 175 | 'a': 'a', 176 | 'b': 'b' 177 | }, 178 | 'authorization': 'secret' 179 | }; 180 | const umfMessage = UMFMessage.createMessage((inMessage)); 181 | const shortMessageId = umfMessage.createShortMessageID(); 182 | expect(shortMessageId).to.be.defined; 183 | done(); 184 | }); 185 | 186 | /** 187 | * @description Create a signed message 188 | */ 189 | it('should create a signature for the message', (done) => { 190 | const inMessage = { 191 | 'to': 'xxx', 192 | 'from': 'yyy', 193 | 'headers': 'aaa', 194 | 'mid': 'mid123', 195 | 'rmid': 'rmid123', 196 | 'type': 'type-http', 197 | 'timestamp': '2018-03-11T16:52:42.060Z', 198 | 'via': 'uid:123', 199 | 'timeout': 3000, 200 | 'forward': 'yyy@aaa', 201 | 'signature': 'test', 202 | 'body': { 203 | 'a': 'a', 204 | 'b': 'b' 205 | }, 206 | 'authorization': 'secret' 207 | }; 208 | const umfMessage = UMFMessage.createMessage((inMessage)); 209 | umfMessage.signMessage('sha256', 'testing'); 210 | expect(umfMessage.signature).to.be.defined; 211 | expect(umfMessage.signature).to.be.equal('4796729993fe18df531d16668505b4fd6741c94c0a9db5116df214d7277d587d'); 212 | done(); 213 | }); 214 | 215 | /** 216 | * @description Should return error for bad route 217 | */ 218 | it('should return simple parsed route', (done) => { 219 | const parseObj = UMFMessage.parseRoute('uid:xxx123'); 220 | expect(parseObj).to.be.object; 221 | expect(parseObj.error).to.be.empty; 222 | expect(parseObj.serviceName).to.equal('uid'); 223 | expect(parseObj.apiRoute).to.equal('xxx123'); 224 | done(); 225 | }); 226 | 227 | /** 228 | * @description Should return error for bad route 229 | */ 230 | it('should return error for bad route', (done) => { 231 | const parseObj = UMFMessage.parseRoute('xx'); 232 | expect(parseObj).to.be.object; 233 | expect(parseObj.error).to.not.be.empty; 234 | done(); 235 | }); 236 | 237 | /** 238 | * @description Should return parsed HTTP route 239 | */ 240 | it('should return good http parsed route', (done) => { 241 | const parseObj = UMFMessage.parseRoute('http:/V1/URL/xxx123:route'); 242 | expect(parseObj).to.be.object; 243 | expect(parseObj.error).to.be.empty; 244 | expect(parseObj.serviceName).to.equal('http:/V1/URL/xxx123'); 245 | expect(parseObj.apiRoute).to.equal('route'); 246 | done(); 247 | }); 248 | 249 | /** 250 | * @description Should return error for bad http route 251 | */ 252 | it('should return error for bad http method route', (done) => { 253 | const parseObj = UMFMessage.parseRoute('http:/V1/URL/xxx123:[route'); 254 | expect(parseObj).to.be.object; 255 | expect(parseObj.error).to.not.be.empty; 256 | done(); 257 | }); 258 | 259 | /** 260 | * @description Should return error for bad http route 261 | */ 262 | it('should return valid parsed route with http method', (done) => { 263 | const parseObj = UMFMessage.parseRoute('http:/V1/URL/xxx123:[get]route'); 264 | expect(parseObj).to.be.object; 265 | expect(parseObj.error).to.be.empty; 266 | expect(parseObj.httpMethod).to.equal('get'); 267 | done(); 268 | }); 269 | 270 | /** 271 | * @description Should return route with @ in it 272 | */ 273 | it('should return valid parsed route with @', (done) => { 274 | const parseObj = UMFMessage.parseRoute('test-subtest@service:xxx:yyy'); 275 | expect(parseObj).to.be.object; 276 | expect(parseObj.error).to.be.empty; 277 | expect(parseObj.instance).to.equal('test'); 278 | expect(parseObj.subID).to.equal('subtest'); 279 | expect(parseObj.serviceName).to.equal('service'); 280 | expect(parseObj.apiRoute).to.equal('xxx:yyy'); 281 | done(); 282 | }); 283 | 284 | 285 | /** 286 | * @description Should return route with @ in it 287 | */ 288 | it('should return valid parsed route with @ but no subid', (done) => { 289 | const parseObj = UMFMessage.parseRoute('test@service:xxx:yyy'); 290 | expect(parseObj).to.be.object; 291 | expect(parseObj.error).to.be.empty; 292 | expect(parseObj.instance).to.equal('test'); 293 | expect(parseObj.subID).to.be.empty; 294 | expect(parseObj.serviceName).to.equal('service'); 295 | expect(parseObj.apiRoute).to.equal('xxx:yyy'); 296 | done(); 297 | }); 298 | 299 | /** 300 | * @description Should return route with empty HTTP method 301 | */ 302 | it('should return valid parsed route with @', (done) => { 303 | const parseObj = UMFMessage.parseRoute('http:/V1/URL/xxx123:[]route'); 304 | expect(parseObj).to.be.object; 305 | expect(parseObj.error).to.be.empty; 306 | expect(parseObj.httpMethod).to.be.empty; 307 | done(); 308 | }); 309 | }); 310 | -------------------------------------------------------------------------------- /specs/utils.test.js: -------------------------------------------------------------------------------- 1 | /* eslint no-invalid-this: 0 */ 2 | 3 | require('./helpers/chai.js'); 4 | 5 | const Utils = require('../lib/utils'); 6 | const SECOND = 1000; 7 | 8 | 9 | /** 10 | * @name Utils Tests 11 | * @summary Utils Test Suite 12 | */ 13 | describe('Utils', function() { 14 | this.timeout(SECOND * 10); 15 | 16 | beforeEach(() => { 17 | }); 18 | 19 | afterEach((done) => { 20 | done(); 21 | }); 22 | 23 | 24 | /** 25 | * @description MD5 should return a valid MD5 hash 26 | */ 27 | it('should return valid MD5 hash', (done) => { 28 | const myMD5 = Utils.md5Hash('TEST_KEY'); 29 | expect(myMD5).to.be.equal('58cf16b25485a0116b85806bba9ca7e4'); 30 | done(); 31 | }); 32 | 33 | /** 34 | * @description safeJSONStringy should return valid JSON string 35 | */ 36 | it('should return valid JSON string', (done) => { 37 | const myData = {'key': 'test'}; 38 | const myJSON = Utils.safeJSONStringify(myData); 39 | expect(myJSON).to.be.equal('{"key":"test"}'); 40 | done(); 41 | }); 42 | 43 | /** 44 | * @description safeJSONStringy should stringify an Error object 45 | */ 46 | it('should return valid JSON Error string', (done) => { 47 | const myError = new Error('OOPS'); 48 | const myData = {'error': myError}; 49 | const myJSON = Utils.safeJSONStringify(myData); 50 | expect(myJSON).to.include('OOPS'); 51 | done(); 52 | }); 53 | 54 | /** 55 | * @description safeJSONParse should return valid JS data if valid JSON 56 | */ 57 | it('should return valid JS data structure', (done) => { 58 | const myData = Utils.safeJSONParse('{"key" : "test"}'); 59 | expect(myData.key).to.be.equal('test'); 60 | done(); 61 | }); 62 | 63 | /** 64 | * @description safeJSONParse should return undefined if invalid JSON 65 | */ 66 | it('should return valid undefined', (done) => { 67 | const myData = Utils.safeJSONParse('{"key" : '); 68 | expect(myData).to.be.undefined; 69 | done(); 70 | }); 71 | 72 | /** 73 | * @description stringHash should return a hash for a string 74 | */ 75 | it('should return the a hash value for a string', (done) => { 76 | const myHash = Utils.stringHash('TEST_STRING'); 77 | expect(myHash).to.be.equal(2282002681); 78 | done(); 79 | }); 80 | 81 | /** 82 | * @description shortID should return a random id from A-Z 0-9. 83 | */ 84 | it('should return an id with only A-Z and 0-9', (done) => { 85 | const myID = Utils.shortID(); 86 | expect(myID).to.be.defined; 87 | done(); 88 | }); 89 | 90 | /** 91 | * @description True for a valid UUID4 string. 92 | */ 93 | it('should return true for valid UUID4 strings', (done) => { 94 | expect(Utils.isUUID4('ABCDEF12-BBBB-CCCC-dddd-1234567890AB')).to.be.true; 95 | done(); 96 | }); 97 | 98 | /** 99 | * @description False for an invalid UUID4 string. 100 | */ 101 | it('should return false for an invalid UUID4 strings', (done) => { 102 | expect(Utils.isUUID4('XBCDEF12-BBBB-CCCC-dddd-1234567890AB')).to.be.false; 103 | done(); 104 | }); 105 | 106 | /** 107 | * @description Shuffle an array in place 108 | */ 109 | it('should be able to shuffle an array in place', (done) => { 110 | const startArray = Array.from(Array(10).keys()); 111 | const shuffleArray = startArray.slice(); 112 | Utils.shuffleArray(shuffleArray); 113 | expect(shuffleArray).to.not.equal(startArray); 114 | done(); 115 | }); 116 | }); 117 | -------------------------------------------------------------------------------- /tests/messaging/blue-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const hydra = require('../../index'); 4 | 5 | const config = { 6 | hydra: { 7 | serviceName: 'blue-service', 8 | serviceDescription: 'Blue test service', 9 | serviceIP: '', 10 | servicePort: 0, 11 | serviceType: 'test', 12 | redis: { 13 | url: '127.0.0.1', 14 | port: 6379, 15 | db: 0 16 | } 17 | } 18 | }; 19 | 20 | let count = 0; 21 | 22 | hydra.init(config.hydra) 23 | .then(() => { 24 | hydra.registerService() 25 | .then((serviceInfo) => { 26 | console.log(`Running ${serviceInfo.serviceName} at ${serviceInfo.serviceIP}:${serviceInfo.servicePort}`); 27 | hydra.on('message', (message) => { 28 | console.log(`Received object message: ${msg.mid}: ${JSON.stringify(msg)}`); 29 | }); 30 | setInterval(() => { 31 | hydra.sendMessage(hydra.createUMFMessage({ 32 | to: 'red-service:/', 33 | from: 'blue-service:/', 34 | body: { 35 | count 36 | } 37 | })); 38 | count += 1; 39 | }, 2000); 40 | }); 41 | }); 42 | -------------------------------------------------------------------------------- /tests/messaging/red-service.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const hydra = require('../../index'); 4 | 5 | const config = { 6 | hydra: { 7 | serviceName: 'red-service', 8 | serviceDescription: 'Red test service', 9 | serviceIP: '', 10 | servicePort: 0, 11 | serviceType: 'test', 12 | redis: { 13 | url: '127.0.0.1', 14 | port: 6379, 15 | db: 0 16 | } 17 | } 18 | }; 19 | 20 | hydra.init(config.hydra) 21 | .then(() => { 22 | hydra.registerService() 23 | .then((serviceInfo) => { 24 | console.log(`Running ${serviceInfo.serviceName} at ${serviceInfo.serviceIP}:${serviceInfo.servicePort}`); 25 | hydra.on('message', (message) => { 26 | console.log(`Received object message: ${message.mid}: ${JSON.stringify(message)}`); 27 | }); 28 | }); 29 | }); 30 | --------------------------------------------------------------------------------