├── .babelrc ├── .github └── ISSUE_TEMPLATE │ └── bug_report.md ├── .gitignore ├── .npmignore ├── .snyk ├── LICENSE ├── README.md ├── benchmarks ├── pigato │ ├── pigato-broker.js │ ├── pigato-client.js │ └── pigato-worker.js ├── seneca │ ├── seneca-client.js │ └── seneca-server.js └── zeronode │ ├── zeronode-client.js │ └── zeronode-server.js ├── docs ├── CODE_OF_CONDUCT.md ├── CONFIGURE.md ├── CONTRIBUTING.md ├── Chanchelog.md ├── ENVELOP.md ├── METRICS.md ├── MIDDLEWARE.md └── TODO.md ├── examples ├── node-cycle.js ├── objectFilter.js ├── predicateFilter.js ├── regexpFilter.js ├── request-error.js ├── request-many-handlers.js ├── requestAny.js ├── simple-request.js ├── simple-tick.js ├── tickAll.js └── tickAny.js ├── package-lock.json ├── package.json ├── preinstall.sh ├── src ├── actor.js ├── client.js ├── enum.js ├── errors.js ├── globals.js ├── index.js ├── metric.js ├── node.js ├── server.js ├── sockets │ ├── dealer.js │ ├── enum.js │ ├── envelope.js │ ├── events.js │ ├── example │ │ ├── bug.js │ │ ├── dealer.js │ │ ├── router.js │ │ └── test.js │ ├── index.js │ ├── router.js │ ├── socket.js │ └── watchers.js └── utils.js └── test ├── client-server.js ├── manyToMany.js ├── manyToOne.js ├── metrics.js └── oneToOne.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["@babel/preset-env"], 3 | "plugins" : [ 4 | "@babel/plugin-proposal-function-bind", 5 | "@babel/plugin-proposal-object-rest-spread", 6 | ["@babel/transform-runtime", 7 | { 8 | "helpers": false, 9 | "regenerator": true 10 | } 11 | ] 12 | ], 13 | "env": { 14 | "test": { 15 | "plugins": ["istanbul"] 16 | } 17 | } 18 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # nyc test coverage 18 | .nyc_output 19 | 20 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 21 | .grunt 22 | 23 | # node-waf configuration 24 | .lock-wscript 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directories 30 | node_modules 31 | jspm_packages 32 | 33 | # Optional npm cache directory 34 | .npm 35 | 36 | # Optional REPL history 37 | .node_repl_history 38 | 39 | .idea/* 40 | dist/ 41 | 42 | tmp/ 43 | 44 | # VS code local history 45 | .history/ 46 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | benchmarks/ 3 | coverage/ 4 | examples/ 5 | node_modules/ 6 | src/ 7 | test/ 8 | tmp/ 9 | .babelrc 10 | .gitignore 11 | .npmignore 12 | Chanchelog.md 13 | TODO.md 14 | .idea/ 15 | .history/ 16 | npm-debug.log -------------------------------------------------------------------------------- /.snyk: -------------------------------------------------------------------------------- 1 | # Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities. 2 | version: v1.12.0 3 | # ignores vulnerabilities until expiry date; change duration by modifying expiry date 4 | ignore: 5 | 'npm:chownr:20180731': 6 | - zeromq > prebuild-install > tar-fs > chownr: 7 | reason: >- 8 | Chownr has a recently reported issue to snyk, though the issue itself 9 | has been known for over a year. 10 | expires: '2020-01-18T16:03:09.970Z' 11 | patch: {} 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | (The MIT License) 2 | 3 | Copyright (c) 2018 Steadfast.tech 4 | steadfast.tech 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining 7 | a copy of this software and associated documentation files (the 8 | 'Software'), to deal in the Software without restriction, including 9 | without limitation the rights to use, copy, modify, merge, publish, 10 | distribute, sublicense, and/or sell copies of the Software, and to 11 | permit persons to whom the Software is furnished to do so, subject to 12 | the following conditions: 13 | 14 | The above copyright notice and this permission notice shall be 15 | included in all copies or substantial portions of the Software. 16 | 17 | THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, 18 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 19 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 20 | IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 21 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 22 | TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 23 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Zeronode](https://i.imgur.com/NZVXZPo.png) 2 |
3 | 4 | [![JavaScript Style Guide](https://cdn.rawgit.com/standard/standard/master/badge.svg)](https://github.com/standard/standard)

5 | [![NPM](https://nodei.co/npm/zeronode.png)](https://nodei.co/npm/zeronode/)

6 | 7 | [](https://gitter.im/npm-zeronode/Lobby) 8 | [![Known Vulnerabilities](https://snyk.io/test/github/sfast/zeronode/badge.svg)](https://snyk.io/test/github/sfast/zeronode) 9 | [![GitHub license](https://img.shields.io/github/license/sfast/zeronode.svg)](https://github.com/sfast/zeronode/blob/master/LICENSE) 10 | [![GitHub issues](https://img.shields.io/github/issues/sfast/zeronode.svg)](https://github.com/sfast/zeronode/issues) 11 | 12 | [![Tweet](https://img.shields.io/twitter/url/http/shields.io.svg?style=social)](https://twitter.com/intent/tweet?text=Zeronode%20-%20rock%20solid%20transport%20and%20smarts%20for%20building%20NodeJS%20microservices.%E2%9C%8C%E2%9C%8C%E2%9C%8C&url=https://github.com/sfast/zeronode&hashtags=microservices,scaling,loadbalancing,zeromq,awsomenodejs,nodejs) 13 | [![GitHub stars](https://img.shields.io/github/stars/sfast/zeronode.svg?style=social&label=Stars)](https://github.com/sfast/zeronode) 14 | 15 | 16 | ## Zeronode - minimal building block for NodeJS microservices 17 | * [Why Zeronode?](#whyZeronode) 18 | * [Installation](#installation) 19 | * [Basics](#basics) 20 | * [Benchmark](#benchmark) 21 | * [API](#api) 22 | * [Examples](#examples) 23 | * [Basic Examples](#basicExamples) 24 | * [Basic Examples](#basicExamples) 25 | * [Advanced] (#advanced) 26 | * [Basic Examples](#basicExamples) 27 | * [Basic Examples](#basicExamples) 28 | * [Contributing](#contributing) 29 | * [Have a question ?](#askzeronode) 30 | * [License](#license) 31 | 32 | 33 | ### Why you need ZeroNode ? 34 | Application backends are becoming complex these days and there are lots of moving parts talking to each other through network. 35 | There is a great difference between sending a few bytes from A to B, and doing messaging in reliable way. 36 | - How to handle dynamic components ? (i.e., pieces that come and/or go away temporarily, scaling a microservice instances ) 37 | - How to handle messages that we can't deliver immediately ? (i.e waiting for a component to come back online) 38 | - How to route messages in complex microservice architecture ? (i.e. one to one, one to many, custom grouping) 39 | - How we handle network errors ? (i.e., reconnecting of various pieces) 40 | 41 | We created Zeronode on top of zeromq as to address these 42 | and some more common problems that developers will face once building solid systems. 43 |
44 | With zeronode its just super simple to create complex server-to-server communications (i.e. build network topologies). 45 | 46 | 47 | ### Installation & Important notes 48 | Zeronode depends on zeromq 49 |
For Debian, Ubuntu, MacOS you can just run 50 | ```bash 51 | $ npm install zeronode --save 52 | ``` 53 | and it'll also install [zeromq](http://zeromq.org) for you. 54 |
Kudos to Dave for adding install scripts. 55 | For other platforms please open an issue or feel free to contribute. 56 | 57 | 58 | ### Basics 59 | Zeronode allows to create complex network topologies (i.e. line, ring, partial or full mesh, star, three, hybrid ...) 60 | Each participant/actor in your network topology we call __znode__, which can act as a sever, as a client or hybrid. 61 | 62 | ```javascript 63 | import Node from 'zeronode'; 64 | 65 | let znode = new Node({ 66 | id: 'steadfast', 67 | options: {}, 68 | config: {} 69 | }); 70 | 71 | // ** If znode is binded to some interface then other znodes can connect to it 72 | // ** In this case znode acts as a server, but it's not limiting znode to connect also to other znodes (hybrid) 73 | (async () => { 74 | await znode.bind('tcp://127.0.0.1:6000'); 75 | })(); 76 | 77 | // ** znode can connect to multiple znodes 78 | znode.connect({address: 'tcp://127.0.0.1:6001'}) 79 | znode.connect({address: 'tcp://127.0.0.1:6002'}) 80 | 81 | // ** If 2 znodes are connected together then we have a channel between them 82 | // ** and both znodes can talk to each other via various messeging patterns - i.e. request/reply, tick (fire and forgot) etc ... 83 | 84 | ``` 85 | 86 | Much more interesting patterns and features you can discover by reading the [API](#api) document. 87 | In case you have a question or suggestion you can talk to authors on [Zeronode Gitter chat](#askzeronode) 88 | 89 | 90 | 91 | 92 | ### Benchmark 93 | All Benchmark tests are completed on Intel(R) Core(TM) i5-6200U CPU @ 2.30GHz. 94 | 95 | 96 | 97 | 98 | 99 |
ZeronodeSeneca (tcp)Pigato
1000 msg, 1kb data394ms2054ms342ms
50000 msg, 1kb data11821ms140934msFAIL(100s timeout)
100 |
101 | 102 | 103 | ### API 104 | 105 | #### Basic methods 106 | * [**new Node()**](#node) 107 | * [znode.**bind()**](#bind) 108 | * [znode.**connect()**](#connect) 109 | * [znode.**unbind()**](#unbind) 110 | * [znode.**disconnect()**](#disconnect) 111 | * [znode.**stop()**](#stop) 112 | 113 | #### Simple messaging methods 114 | * [znode.**request()**](#request) 115 | * [znode.**tick()**](#tick) 116 | 117 | #### Attaching/Detaching handlers to tick and request 118 | 119 | * [znode.**onRequest()**](#onRequest) 120 | * [znode.**onTick()**](#onTick) 121 | * [znode.**offRequest()**](#offRequest) 122 | * [znode.**offTick()**](#offTick) 123 | 124 | #### Load balancing methods 125 | 126 | * [znode.**requestAny()**](#requestAny) 127 | * [znode.**requestDownAny()**](#requestDownAny) 128 | * [znode.**requestUpAny()**](#requestUpAny) 129 | * [znode.**tickAny()**](#tickAny) 130 | * [znode.**tickDownAny()**](#tickDownAny) 131 | * [znode.**tickUpAny()**](#tickUpAny) 132 | * [znode.**tickAll()**](#tickAll) 133 | * [znode.**tickDownAll()**](#tickDownAll) 134 | * [znode.**tickUpAll()**](#tickUpAll) 135 | 136 | #### Debugging and troubleshooting 137 | 138 | * [**znode.enableMetrics()**](#enableMetrics) 139 | * [**znode.disableMetrics()**](#disableMetrics) 140 | 141 | 142 | #### let znode = new Node({ id: String, bind: Url, options: Object, config: Object }) 143 | Node class wraps many client instances and one server instance. 144 | Node automatically handles: 145 | * Client/Server ping/pong 146 | * Reconnections 147 | 148 | ```javascript 149 | import { Node } from 'zeronode'; 150 | 151 | let znode = new Node({ 152 | id: 'node', 153 | bind: 'tcp://127.0.0.1:6000', 154 | options: {} 155 | config: {} 156 | }); 157 | ``` 158 | 159 | All four arguments are optional. 160 | * `id` is unique string which identifies znode. 161 | * `options` is information about znode which is shared with other connected znoded. It could be used for advanced use cases of load balancing and messege routing. 162 | * `config` is an object for configuring znode 163 | * `logger` - logger instance, default is Winston. 164 | * `REQUEST_TIMEOUT` - duration after which request()-s promise will be rejected, default is 10,000 ms. 165 | * `RECONNECTION_TIMEOUT` (for client znodes) - zeronode's default is -1 , which means zeronode is always trying to reconnect to failed znode server. Once `RECONNECTION_TIMEOUT` is passed and recconenction doesn't happen zeronode will fire `SERVER_RECONNECT_FAILURE`. 166 | * `CONNECTION_TIMEOUT` (for client znodes) - duration for trying to connect to server after which connect()-s promise will be rejected. 167 | 168 | There are some events that triggered on znode instances: 169 | * `NodeEvents.`**`CLIENT_FAILURE`** - triggered on server znode when client connected to it fails. 170 | * `NodeEvents.`**`CLIENT_CONNECTED`** - triggered on server znode when new client connects to it. 171 | * `NodeEvents.`**`CLIENT_STOP`** - triggered on server znode when client successfully disconnects from it. 172 | 173 | * `NodeEvents.`**`SERVER_FAILURE`** - triggered on client znode when server znode fails. 174 | * `NodeEvents.`**`SERVER_STOP`** - triggered on client znode when server successfully stops. 175 | * `NodeEvents.`**`SERVER_RECONNECT`** - triggered on client znode when server comes back and client znode successfuly reconnects. 176 | * `NodeEvents.`**`SERVER_RECONNECT_FAILURE`** - triggered on client znode when server doesn't come back in `reconnectionTimeout` time provided during connect(). If `reconnectionTimeout` is not provided it uses `config.RECONNECTION_TIMEOUT` which defaults to -1 (means client znode will try to reconnect to server znode for ages). 177 | * `NodeEvents.`**`CONNECT_TO_SERVER`** - triggered on client znode when it successfully connects to new server. 178 | * `NodeEvents.`**`METRICS`** - triggered when [metrics enabled](#enableMetrics). 179 | 180 | 181 | 182 | #### znode.bind(address: Url) 183 | Binds the znode to the specified interface and port and returns promise. 184 | You can bind only to one address. 185 | Address can be of the following protocols: `tcp`, `inproc`(in-process/inter-thread), `ipc`(inter-process). 186 | 187 | 188 | #### znode.connect({ address: Url, timeout: Number, reconnectionTimeout: Number }) 189 | Connects the znode to server znode with specified address and returns promise. 190 | znode can connect to multiple znodes. 191 | If timeout is provided (in milliseconds) then the _connect()-s_ promise will be rejected if connection is taking longer.
192 | If timeout is not provided it will wait for ages till it connects. 193 | If server znode fails then client znode will try to reconnect in given `reconnectionTimeout` (defaults to `RECONNECTION_TIMEOUT`) after which the `SERVER_RECONNECT_FAILURE` event will be triggered. 194 | 195 | 196 | #### znode.unbind() 197 | Unbinds the server znode and returns promise. 198 | Unbinding doesn't stop znode, it can still be connected to other nodes if there are any, it just stops the server behaviour of znode, and on all the client znodes (connected to this server znode) `SERVER_STOP` event will be triggered. 199 | 200 | 201 | #### znode.disconnect(address: Url) 202 | Disconnects znode from specified address and returns promise. 203 | 204 | 205 | #### znode.stop() 206 | Unbinds znode, disconnects from all connected addresses (znodes) and returns promise. 207 | 208 | 209 | #### znode.request({ to: Id, event: String, data: Object, timeout: Number }) 210 | Makes request to znode with id(__to__) and returns promise.
211 | Promise resolves with data that the requested znode replies.
212 | If timeout is not provided it'll be `config.REQUEST_TIMEOUT` (defaults to 10000 ms).
213 | If there is no znode with given id, than promise will be rejected with error code `ErrorCodes.NODE_NOT_FOUND`. 214 | 215 | 216 | #### znode.tick({ to: Id, event: String, data: Object }) 217 | Ticks(emits) event to given znode(__to__).
218 | If there is no znode with given id, than throws error with code `ErrorCodes.NODE_NOT_FOUND`. 219 | 220 | 221 | #### znode.onRequest(requestEvent: String/Regex, handler: Function) 222 | Adds request handler for given event on znode. 223 | ```javascript 224 | /** 225 | * @param head: { id: String, event: String } 226 | * @param body: {} - requestedData 227 | * @param reply(replyData: Object): Function 228 | * @param next(error): Function 229 | */ 230 | // ** listening for 'foo' event 231 | znode.onRequest('foo', ({ head, body, reply, next }) => { 232 | // ** request handling logic 233 | // ** move forward to next handler or stop the handlers chain with 'next(err)' 234 | next() 235 | }) 236 | 237 | // ** listening for any events matching Regexp 238 | znode.onRequest(/^fo/, ({ head, body, reply, next }) => { 239 | // ** request handling logic 240 | // ** send back reply to the requester znode 241 | reply(/* Object data */) 242 | }) 243 | ``` 244 | 245 | 246 | #### znode.onTick(event: String/Regex, handler: Function) 247 | Adds tick(event) handler for given event. 248 | ```javascript 249 | znode.onTick('foo', (data) => { 250 | // ** tick handling logic 251 | }) 252 | ``` 253 | 254 | 255 | #### znode.offRequest(requestEvent: String/Regex, handler: Function) 256 | Removes request handler for given event.
257 | If handler is not provided then removes all of the listeners. 258 | 259 | 260 | #### znode.offTick(event: String/Regex, handler: Function) 261 | Removes given tick(event) handler from event listeners' list.
262 | If handler is not provided then removes all of the listeners. 263 | 264 | 265 | #### znode.requestAny({ event: String, data: Object, timeout: Number, filter: Object/Function, down: Bool, up: Bool }) 266 | General method to send request to __only one__ znode satisfying the filter.
267 | Filter can be an object or a predicate function. Each filter key can be object itself, with this keys. 268 | - **$eq** - strict equal to provided value. 269 | - **$ne** - not equal to provided value. 270 | - **$aeq** - loose equal to provided value. 271 | - **$gt** - greater than provided value. 272 | - **$gte** - greater than or equal to provided value. 273 | - **$lt** - less than provided value. 274 | - **$lte** - less than or equal to provided value. 275 | - **$between** - between provided values (value must be tuple. eg [10, 20]). 276 | - **$regex** - match to provided regex. 277 | - **$in** - matching any of the provided values. 278 | - **$nin** - not matching any of the provided values. 279 | - **$contains** - contains provided value. 280 | - **$containsAny** - contains any of the provided values. 281 | - **$containsNone** - contains none of the provided values. 282 | 283 | ```javascript 284 | // ** send request to one of znodes that have version 1.*.* 285 | znode.requestAny({ 286 | event: 'foo', 287 | data: { foo: 'bar' }, 288 | filter: { version: /^1.(\d+\.)?(\d+)$/ } 289 | }) 290 | 291 | // ** send request to one of znodes whose version is greater than 1.0.0 292 | znode.requestAny({ 293 | event: 'foo', 294 | data: { foo: 'bar' }, 295 | filter: { version: { $gt: '1.0.0' } } 296 | }) 297 | 298 | // ** send request to one of znodes whose version is between 1.0.0 and 2.0.0 299 | znode.requestAny({ 300 | event: 'foo', 301 | data: { foo: 'bar' }, 302 | filter: { version: { $between: ['1.0.0', '2.0.0.'] } } 303 | }) 304 | 305 | // ** send request to one of znodes that have even length of name. 306 | znode.requestAny({ 307 | event: 'foo', 308 | data: { foo: 'bar' }, 309 | filter: (options) => !(options.name.length % 2) 310 | }) 311 | 312 | // ** send request to one of znodes that connected to your znode (downstream client znodes) 313 | znode.requestAny({ 314 | event: 'foo', 315 | data: { foo: 'bar' }, 316 | up: false 317 | }) 318 | 319 | // ** send request to one of znodes that your znode is connected to (upstream znodes). 320 | znode.requestAny({ 321 | event: 'foo', 322 | data: { foo: 'bar' }, 323 | down: false 324 | }) 325 | ``` 326 | 327 | 328 | #### znode.requestDownAny({ event: String, data: Object, timeout: Number, filter: Object/Function }) 329 | Send request to one of downstream znodes (znodes which has been connected to your znode via _connect()_ ). 330 | 331 | 332 | 333 | #### znode.requestUpAny({ event: String, data: Object, timeout: Number, filter: Object/Function }) 334 | Send request to one of upstream znodes (znodes to which your znode has been connected via _connect()_ ). 335 | 336 | 337 | #### znode.tickAny({ event: String, data: Object, filter: Object/Function, down: Bool, up: Bool }) 338 | General method to send tick-s to __only one__ znode satisfying the filter.
339 | Filter can be an object or a predicate function. 340 | Usage is same as [`node.requestAny`](#requestAny) 341 | 342 | 343 | #### znode.tickDownAny({ event: String, data: Object, filter: Object/Function }) 344 | Send tick-s to one of downstream znodes (znodes which has been connected to your znode via _connect()_ ). 345 | 346 | 347 | #### znode.tickUpAny({ event: String, data: Object, filter: Object/Function }) 348 | Send tick-s to one of upstream znodes (znodes to which your znode has been connected via _connect()_ ). 349 | 350 | 351 | #### znode.tickAll({ event: String, data: Object, filter: Object/Function, down: Bool, up: Bool }) 352 | Tick to **ALL** znodes satisfying the filter (object or predicate function), up ( _upstream_ ) and down ( _downstream_ ). 353 | 354 | 355 | #### znode.tickDownAll({ event: String, data: Object, filter: Object/Function }) 356 | Tick to **ALL** downstream znodes. 357 | 358 | 359 | #### znode.tickUpAll({ event: String, data: Object, filter: Object/Function }) 360 | Tick to **ALL** upstream znodes. 361 | 362 | 363 | #### znode.enableMetrics(interval) 364 | Enables metrics, events will be triggered by the given interval. Default interval is 1000 ms.
365 | 366 | 367 | #### znode.disableMetrics() 368 | Stops triggering events, and removes all collected data. 369 | 370 | 371 | ### Examples 372 | 373 | #### Simple client server example 374 | NodeServer is listening for events, NodeClient connects to NodeServer and sends events:
375 | (myServiceClient) ----> (myServiceServer) 376 | 377 | Lets create server first 378 | 379 | myServiceServer.js 380 | ```javascript 381 | import Node from 'zeronode'; 382 | 383 | (async function() { 384 | let myServiceServer = new Node({ id: 'myServiceServer', bind: 'tcp://127.0.0.1:6000', options: { layer: 'LayerA' } }); 385 | 386 | // ** attach event listener to myServiceServer 387 | myServiceServer.onTick('welcome', (data) => { 388 | console.log('onTick - welcome', data); 389 | }); 390 | 391 | // ** attach request listener to myServiceServer 392 | myServiceServer.onRequest('welcome', ({ head, body, reply, next }) => { 393 | console.log('onRequest - welcome', body); 394 | reply("Hello client"); 395 | next(); 396 | }); 397 | 398 | // second handler for same channel 399 | myServiceServer.onRequest('welcome', ({ head, body, reply, next }) => { 400 | console.log('onRequest second - welcome', body); 401 | }); 402 | 403 | // ** bind znode to given address provided during construction 404 | await myServiceServer.bind(); 405 | }()); 406 | 407 | ``` 408 | Now lets create a client 409 | 410 | myServiceClient.js 411 | ```javascript 412 | import Node from 'zeronode' 413 | 414 | (async function() { 415 | let myServiceClient = new Node({ options: { layer: 'LayerA' } }); 416 | 417 | //** connect one node to another node with address 418 | await myServiceClient.connect({ address: 'tcp://127.0.0.1:6000' }); 419 | 420 | let serverNodeId = 'myServiceServer'; 421 | 422 | // ** tick() is like firing an event to another node 423 | myServiceClient.tick({ to: serverNodeId, event: 'welcome', data:'Hi server!!!' }); 424 | 425 | // ** you request to another node and getting a promise 426 | // ** which will be resolve after reply. 427 | let responseFromServer = await myServiceClient.request({ to: serverNodeId, event: 'welcome', data: 'Hi server, I am client !!!' }); 428 | 429 | console.log(`response from server is "${responseFromServer}"`); 430 | // ** response from server is "Hello client." 431 | }()); 432 | 433 | ``` 434 | 435 | 436 | #### Example of filtering the znodes via options. 437 | 438 | Let's say we want to group our znodes logicaly in some layers and send messages considering that layering. 439 | - __znode__-s can be grouped in layers (and other options) and then send messages to only filtered nodes by layers or other options. 440 | - the filtering is done on senders side which keeps all the information about the nodes (both connected to sender node and the ones that 441 | sender node is connected to) 442 | 443 | In this example, we will create one server znode that will bind in some address, and three client znodes will connect to our server znode. 444 | 2 of client znodes will be in layer `A`, 1 in `B`. 445 | 446 | serverNode.js 447 | ```javascript 448 | import Node from 'zeronode' 449 | 450 | (async function() { 451 | let server = new Node({ bind: 'tcp://127.0.0.1:6000' }); 452 | await server.bind(); 453 | }()); 454 | ``` 455 | 456 | clientA1.js 457 | ```javascript 458 | import Node from 'zeronode' 459 | 460 | (async function() { 461 | let clientA1 = new Node({ options: { layer: 'A' } }); 462 | 463 | clientA1.onTick('foobar', (msg) => { 464 | console.log(`go message in clientA1 ${msg}`); 465 | }); 466 | 467 | // ** connect to server address and set connection timeout to 20 seconds 468 | await clientA1.connect({ address: 'tcp:://127.0.0.1:6000', 20000 }); 469 | }()); 470 | ``` 471 | 472 | clientA2.js 473 | ```javascript 474 | import Node from 'zeronode' 475 | 476 | (async function() { 477 | let clientA2 = new Node({ options: { layer: 'A' } }); 478 | 479 | clientA2.onTick('foobar', (msg) => { 480 | console.log(`go message in clientA2 ${msg}`); 481 | }); 482 | // ** connect to server address and set connection timeout infinite 483 | await clientA2.connect({ address: 'tcp:://127.0.0.1:6000') }; 484 | }()); 485 | ``` 486 | 487 | clientB1.js 488 | ```javascript 489 | import Node from 'zeronode' 490 | 491 | (async function() { 492 | let clientB1 = new Node({ options: { layer: 'B' } }); 493 | 494 | clientB1.onTick('foobar', (msg) => { 495 | console.log(`go message in clientB1 ${msg}`); 496 | }); 497 | 498 | // ** connect to server address and set connection timeout infinite 499 | await clientB1.connect({ address: 'tcp:://127.0.0.1:6000' }); 500 | }()); 501 | ``` 502 | 503 | Now that all connections are set, we can send events. 504 | ```javascript 505 | // ** this will tick only one node of the layer A nodes; 506 | server.tickAny({ event: 'foobar', data: { foo: 'bar' }, filter: { layer: 'A' } }); 507 | 508 | // ** this will tick to all layer A nodes; 509 | server.tickAll({ event: 'foobar', data: { foo: 'bar' }, filter: { layer: 'A' } }); 510 | 511 | // ** this will tick to all nodes that server connected to, or connected to server. 512 | server.tickAll({ event: 'foobar', data: { foo: 'bar' } }); 513 | 514 | 515 | // ** you even can use regexp to filer znodes to which the tick will be sent 516 | // ** also you can pass a predicate function as a filter which will get znode-s options as an argument 517 | server.tickAll({ event: 'foobar', data: { foo: 'bar' }, filter: {layer: /[A-Z]/} }) 518 | ``` 519 | 520 | 521 | ### Still have a question ? 522 | We'll be happy to answer your questions. Try to reach out us on zeronode gitter chat [](https://gitter.im/npm-zeronode/Lobby)
523 | 524 | 525 | ### Contributing 526 | Contributions are always welcome!
527 | Please read the [contribution guidelines](https://github.com/sfast/zeronode/blob/master/docs/CONTRIBUTING.md) first. 528 | 529 | ### Contributors 530 | * [Artak Vardanyan](https://github.com/artakvg) 531 | * [David Harutyunyan](https://github.com/davidharutyunyan) 532 | 533 | ### More about zeronode internals 534 | Under the hood we are using zeromq-s Dealer and Router sockets. 535 | 536 | 537 | ### License 538 | [MIT](https://github.com/sfast/zeronode/blob/master/LICENSE) 539 | -------------------------------------------------------------------------------- /benchmarks/pigato/pigato-broker.js: -------------------------------------------------------------------------------- 1 | import { Broker } from 'pigato' 2 | 3 | let broker = new Broker('tcp://*:8000') 4 | broker.on('start', () => { 5 | console.log('broker started') 6 | }) 7 | broker.start() -------------------------------------------------------------------------------- /benchmarks/pigato/pigato-client.js: -------------------------------------------------------------------------------- 1 | import { Client } from 'pigato' 2 | import _ from 'underscore' 3 | 4 | let client = new Client('tcp://127.0.0.1:8000') 5 | 6 | client.on('connect', () => { 7 | let count = 0 8 | , start = Date.now() 9 | 10 | _.each(_.range(50000), () => { 11 | client.request('foo', new Buffer(1000), { timeout: 100000}) 12 | .on('data', (...resp) => { 13 | count++ 14 | count === 50000 && console.log(Date.now() - start) 15 | }) 16 | }) 17 | console.log('client successfully connected.') 18 | }) 19 | 20 | client.start() -------------------------------------------------------------------------------- /benchmarks/pigato/pigato-worker.js: -------------------------------------------------------------------------------- 1 | import { Worker } from 'pigato' 2 | 3 | let worker = new Worker('tcp://127.0.0.1:8000', 'foo', { 4 | concurrency: -1 5 | }) 6 | 7 | worker.on('connect', () => { 8 | console.log('Worker successfully connected.') 9 | }) 10 | 11 | worker.on('request', (msg, reply) => { 12 | reply.write(new Buffer(1000)) 13 | reply.end() 14 | }) 15 | 16 | worker.start() -------------------------------------------------------------------------------- /benchmarks/seneca/seneca-client.js: -------------------------------------------------------------------------------- 1 | import Seneca from 'seneca' 2 | import _ from 'underscore' 3 | 4 | let seneca = Seneca({timeout: 1000000}) 5 | seneca.client({port: 9000, type: 'tcp'}) 6 | let start = Date.now() 7 | 8 | let count = 0 9 | _.each(_.range(50000), () => { 10 | seneca.act('foo:bar', new Buffer(1000), (err, resp) => { 11 | count++; 12 | count === 50000 && console.log(Date.now() - start) 13 | }) 14 | }) -------------------------------------------------------------------------------- /benchmarks/seneca/seneca-server.js: -------------------------------------------------------------------------------- 1 | import Seneca from 'seneca' 2 | 3 | let seneca = Seneca({timeout: 1000000}); 4 | 5 | seneca.add('foo:*', (msg, reply) => { 6 | // console.log('received request:', msg) 7 | reply(new Buffer(1000)) 8 | }) 9 | 10 | seneca.listen({port: 9000, type: 'tcp'}) -------------------------------------------------------------------------------- /benchmarks/zeronode/zeronode-client.js: -------------------------------------------------------------------------------- 1 | import Node from '../../src' 2 | import _ from 'underscore' 3 | 4 | let node = new Node(); 5 | let start 6 | 7 | node.connect({ address: 'tcp://127.0.0.1:7000' }) 8 | .then(() => { 9 | console.log('successfully started') 10 | start = Date.now() 11 | return Promise.all(_.map(_.range(50000), () => node.requestAny({ 12 | event: 'foo', 13 | data: new Buffer(1000) 14 | }))) 15 | }) 16 | .then(() => { 17 | console.log(Date.now() - start) 18 | }) 19 | .catch(err => { 20 | console.log(err) 21 | }) -------------------------------------------------------------------------------- /benchmarks/zeronode/zeronode-server.js: -------------------------------------------------------------------------------- 1 | import Node from '../../src' 2 | 3 | let node = new Node(); 4 | 5 | node.bind('tcp://*:7000') 6 | .then(() => { 7 | node.onRequest('foo', ({ body, reply }) => { 8 | reply(new Buffer(1000)) 9 | }) 10 | console.log('successfully started') 11 | }) -------------------------------------------------------------------------------- /docs/CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to making participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies both within project spaces and in public spaces 49 | when an individual is representing the project or its community. Examples of 50 | representing a project or community include using an official project e-mail 51 | address, posting via an official social media account, or acting as an appointed 52 | representative at an online or offline event. Representation of a project may be 53 | further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at hi@steadfast.tech. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | 75 | For answers to common questions about this code of conduct, see 76 | https://www.contributor-covenant.org/faq 77 | -------------------------------------------------------------------------------- /docs/CONFIGURE.md: -------------------------------------------------------------------------------- 1 | ### How to configure zeronode 2 | 3 | Zeronode can be configured by config object given in constructor. 4 | ```javascript 5 | import Node from 'zeronode' 6 | 7 | let node = new Node({ config: {/* some configurations */}}) 8 | ``` 9 | 10 | ### What can be configured 11 | 12 | #### logger 13 | 14 | Default logger of zeronode is Winston. 15 |
16 | Logger can be changed, by giving new logger in configs. 17 | ```javascript 18 | import Node from 'zeronode' 19 | 20 | let node = new Node({ config: { logger: console }}) 21 | ``` 22 | 23 | #### CLIENT_PING_INTERVAL 24 | CLIENT_PING_INTERVAL is interval when client pings to server. 25 |
26 | Default value is 2000ms. 27 | 28 | #### CLIENT_MUST_HEARTBEAT_INTERVAL 29 | CLIENT_MUST_HEARTBEAT_INTERVAL is heartbeat check interval 30 | in which client must heartbeat to server at least once. 31 | 32 | #### CONNECTION_TIMEOUT 33 | CONNECTION_TIMEOUT is timeout for client trying to connect server. 34 |
35 | Default value is -1 (infinity) 36 | 37 | ``` javascript 38 | import Node from 'zeronode' 39 | 40 | let configuredNode = new Node({ 41 | CONNECTION_TIMEOUT: 10*000 42 | }) 43 | 44 | let node = new Node() 45 | 46 | configureNode.connect({ address: 'tcp://127.0.0.1:3000' }) // promise will be rejected after 10 seconds. 47 | configureNode.connect({ address: 'tcp://127.0.0.1:3000', timeout: 5000 }) // promise will be rejected after 5 seconds. 48 | node.connect({ address: 'tcp://127.0.0.1:3000' }) // promise never will be rejected. 49 | 50 | ``` 51 | 52 | #### RECONNECTION_TIMEOUT 53 | RECONNECTION_TIMEOUT is timeout that client waits server, after server fail or stop. 54 |
55 | Default value is -1 (infinity) 56 | 57 | ``` javascript 58 | import Node from 'zeronode' 59 | 60 | let configuredNode = new Node({ 61 | RECONNECTION_TIMEOUT: 10*000 62 | }) 63 | 64 | let node = new Node() 65 | 66 | configureNode.connect({ address: 'tcp://127.0.0.1:3000' }) // will wait server 10 seconds. 67 | configureNode.connect({ address: 'tcp://127.0.0.1:3000', timeout: 5000 }) // will wait server 5 seconds. 68 | node.connect({ address: 'tcp://127.0.0.1:3000' }) // will wait server for ages. 69 | 70 | ``` 71 | 72 | #### REQUEST_TIMEOUT 73 | REQUEST_TIMEOUT is global timeout for rejecting request if there isn't reply. 74 |
75 | Default value is 10000ms. 76 |
77 | REQUEST_TIMEOUT can't be infinity. 78 | 79 | ``` javascript 80 | import Node from 'zeronode' 81 | 82 | let configuredNode = new Node({ 83 | REQUEST_TIMEOUT: 15 * 1000 84 | }) 85 | 86 | let node = new Node() 87 | 88 | configuredNode.requestAny({ 89 | event: 'foo', 90 | timeout: 5000 91 | }) // request will fail after 5 seconds. 92 | 93 | configuredNode.requestAny({ 94 | event: 'foo' 95 | }) // request will fail after 15 seconds. 96 | 97 | node.requestAny({ 98 | event: 'foo' 99 | }) // request will fail after 10 seconds. 100 | 101 | ``` 102 | 103 | #### MONITOR_TIMEOUT 104 | MONITOR_TIMEOUT is zmq.monitor timeout. 105 |
106 | Default value is 10ms. 107 | 108 | #### MONITOR_RESTART_TIMEOUT 109 | MONITOR_RESTART_TIMEOUT is zmq.monitor restart timeout, if first start throws error. 110 |
111 | Default value is 1000ms. 112 | 113 | -------------------------------------------------------------------------------- /docs/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Rules 2 | 1. **No `--force` pushes** or modifying the git history in any way 3 | 2. Follow existing code style 4 | 3. Pull requests with tests are much more likely to be accepted 5 | 4. Follow the guidelines below 6 | 7 | ## Bugfix or Feature? 8 | 9 | This project uses the [gitflow workflow](https://www.atlassian.com/git/tutorials/comparing-workflows/gitflow-workflow). Simply put, you need to decide if your contribution will be a bug fix that could be released as a patch, or a feature that will end up being a minor or major release. 10 | 11 | ### Found a bug that can be fixed without affecting the API? 12 | 13 | 1. **Fork** this repo 14 | 2. Create a new branch from `master` to work in 15 | 3. **Add tests** if needed 16 | 4. Make sure your code **lints** by running `npm run lint` 17 | 5. Make sure your code **passes tests** by running `npm test` 18 | 6. Submit a **pull request** against the `master` branch 19 | 20 | ### New feature or anything that would result in a change to the API? 21 | 22 | 1. **Fork** this repo 23 | 2. Create a new branch from `develop` to work in 24 | 3. **Add tests** to as needed 25 | 4. Make sure your code **lints** by running `npm run standard` 26 | 5. Make sure your code **passes tests** by running `npm run test` 27 | 6. Submit a **pull request** against the `develop` branch 28 | 29 | ## Releases 30 | 31 | Declaring formal releases remains the prerogative of the project maintainer. 32 | 33 | ### Positive environment 34 | 1. Using welcoming and inclusive language 35 | 2. Being respectful of differing viewpoints and experiences 36 | 3. Gracefully accepting constructive criticism 37 | 4. Focusing on what is best for the community 38 | 5. Showing empathy towards other community members 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /docs/Chanchelog.md: -------------------------------------------------------------------------------- 1 | Version: 1.1.31 (June 23 2019, Artak Vardanyan) 2 | - added Metric documentation 3 | - removed the vulneribility we found couple of days ago 4 | 5 | Version: 1.1.7 (April 8 2018, Artak Vardanyan, David Harutyunyan) 6 | - added way to reject request with .error(err) 7 | - changelog date fixed 8 | - added metrics collection 9 | 10 | Version: 1.1.6 (Feb 9 2018, Artak Vardanyan, David Harutyunyan) 11 | - bug fix 12 | - test coverage ~ 90% 13 | - Readme updates 14 | - benchmark 15 | 16 | Version: 1.1.5 (Dec 22 2017, Artak Vardanyan, David Harutyunyan) 17 | - test coverage ~ 70% 18 | - bug fix 19 | - Readme updates 20 | 21 | Version: 1.1.4 (Dec 9 2017, Artak Vardanyan, David Harutyunyan) 22 | - fixed a monitor bug (changed zmq package to zeromq package ) 23 | - added getClientInfo andgetServerInfo node functions which return the actor info 24 | - all node events will emit the full actor information (online, options etc ...) 25 | - from now on we'll tag all releases as a tagged branch to keep it transparent the changes between version 26 | 27 | Version: 1.1.0 (Dec 6 2017, Artak Vardanyan, David Harutyunyan) 28 | - Breaking changes for request and tick methods and 29 | - CLIENT_PING_INTERVAL can be set for the client through client setOptions(options) 30 | - Fixed onRequest() handlers ordering 31 | - added snyk test for vulnerabilities testing 32 | - added zeromq monitor events 33 | 34 | Version: 1.0.12 (Nov 17 2017, Artak Vardanyan, David Harutyunyan) 35 | - added Buffer.aloc shim so zeronode will work also for node < 4.5.0 36 | - added preinstall script for zeroMQ so it'll be installed automatically((works on Debian, Mac) ) -------------------------------------------------------------------------------- /docs/ENVELOP.md: -------------------------------------------------------------------------------- 1 | ### About 2 | Every message in zeronode is encrypted byte array, which contains information 3 | about sender node, receiver node, message data, event, etc... 4 | 5 | ### Structure 6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 |
is_internalevent_typeid_lengthidsender_lengthsender idreceiver_lengthreceiver idevent_lengtheventdata
Typeboolint8int8hexint8utf-8int8utf-8int8utf-8JSON
length (bytes)111id_length1sender_length1receiver_length1event_lengthremaining
50 |
51 |
52 |
53 | 54 | 1. First byte of envelop is describing if message is zeronode's internal event or a custom event - 1 is internal event, 0 is custom event. 55 | 2. Next 1 byte (**Int8**) is holding the information about envelop **type** - 1 is tick, 2 is request, 3 is response, 4 is error. 56 | 3. Next bytes are defining the envelop **id**. 57 | 1. First 1 byte is **Int8** and it's value contains the **length** of bytes after it holding envelop id. 58 | 2. Next **length** of bytes hold the **id** of envelop which is **hex** encoded. 59 | 4. Next bytes are defining the sender node id. 60 | 1. Frst 1 byte is **Int8** and it's value contains the **length** of bytes after it holding sender id. 61 | 2. Next **length** of bytes hold the **id** of sender node, which is **utf-8** encoded. 62 | 5. Next bytes are defining the receiver node id. 63 | 1. Frst 1 byte is **Int8** and it's value contains the **length** of bytes after it holding receiver id. 64 | 2. Next **length** of bytes hold the **id** of receiver node, which is **utf-8** encoded. 65 | 6. Next bytes are defining the **event** name. 66 | 1. Frst 1 byte is **Int8** and it's value contains the **length** of bytes after it holding event name. 67 | 2. Next **length** of bytes hold the **event** name in **utf-8** encoded format. 68 | 7. Remaining bytes are the message data in JSON stringified format. 69 | -------------------------------------------------------------------------------- /docs/METRICS.md: -------------------------------------------------------------------------------- 1 | ### About 2 | Metrics in Zeronode is for collecting information about performance and data traffic. 3 | 4 | ### How to use 5 | Enabling metrics is very simple. 6 | ```javascript 7 | let node = new Node() 8 | node.enableMetrics(flushInterval) //flushInterval is interval for udating aggregation table 9 | ``` 10 | When metrics is enabled, then node will start collecting information about every tick and request. 11 | It will collect data about request fail, timeout, latency, size and so on. 12 | After flushInterval it will aggregate all collected information into one table with event and nodeId and with custom defined column. 13 | 14 | After enabling Metrics, you can query and get information 15 | ```javascript 16 | node.metric.getMetrics(query) 17 | /* 18 | query is loki.js query object. Query is performed on aggregation table. 19 | */ 20 | ``` 21 | 22 | ### stored Data 23 | There are three tables stored in loki.js: Request, Tick, Aggregation. 24 |
25 | All Ticks in flushInterval are stored in Tick table, stored data is 26 | ```javascript 27 | Tick: { 28 | id: hex, // id of tick 29 | event: String, // event name 30 | from: String, // node id that emits event 31 | to: String, // node id that handles event 32 | size: Number // size of message in bytes 33 | } 34 | ``` 35 | 36 | All requests in flushInterval are stored in Request table, stored data is 37 | ```javascript 38 | Request: { 39 | id: hex, // id of request 40 | event: String, // event name 41 | from: String, // node id that makes request 42 | to: String, // node id that handles request 43 | size: Array[Number,Number], // size of request, and reply 44 | timeout: Boolean, // is request timeouted 45 | duration: Object {latency, process}, // time passed in nanoseconds for handling request 46 | success: Boolean // is request succeed , 47 | error: Boolean // is request failed 48 | } 49 | ``` 50 | 51 | All aggregations are stored in Aggregation table. 52 | ```javascript 53 | // request aggregations 54 | { 55 | node: String, //id of node 56 | event: String, // event name 57 | out: Boolean, // request sent or received 58 | request: true, // is request or tick, 59 | latency: Number, // average latency in nanoseconds 60 | process: Number, // averahe process time in nanoseconds 61 | count: Number, // count of requests 62 | success: Number, // count of succed requests 63 | error: Number, // count of failed requests 64 | timeout: Number, // caount of timeouted requests 65 | size: Number // average size of request 66 | customField // custom defined field 67 | } 68 | 69 | // tick aggregations 70 | { 71 | node: String, //id of node 72 | event: String, // event name 73 | out: Boolean, // tick sent or received 74 | request: false, // is request or tick 75 | count: Number, // count of ticks 76 | size: Number // average size of tick 77 | } 78 | ``` 79 | 80 | ### How to define column 81 | You can define custom column in aggregation table, for collecting specific metrics. 82 | ```javascript 83 | node.metric.defineColumn(columnName: String, initialValue, reducer: function, isIndex: Boolean) 84 | /* 85 | columenName: is name of column. 86 | initialvalue: is initial value of column, used in reducer. 87 | reducer: function that called for updating column. 88 | isIndex: is column indexed in loki.js (indexed columns are faster when making queries) 89 | 90 | reducers first parameter is row, second parameter is requesy/tick record. 91 | */ 92 | 93 | ``` 94 | 95 | 96 | ### Examples 97 | 98 | Define Column 99 | ```javascript 100 | let node = new Node() 101 | node.metric.defineColumn('foo', 0, (row, record) => { 102 | //update value, by using row.foo value and record info. 103 | }, true) 104 | 105 | //this will create column with name foo and 0 initial value 106 | ``` 107 | 108 |
109 | 110 | Make query 111 | ```javasript 112 | let node = new Node() 113 | node.enableMetrics(100) 114 | let { result, total }node.metric.getMetrics({ 115 | request: true, 116 | out: true, 117 | latency: {'$lt': 10e9} 118 | }) 119 | 120 | // this query will return all sent request rows, that have latency lower than 1 second. 121 | ``` -------------------------------------------------------------------------------- /docs/MIDDLEWARE.md: -------------------------------------------------------------------------------- 1 | ### How to use middleware 2 | In zeronode there are two messaging types, request and tick. 3 | While tick is simple event emitter, request handlers are successively called and can be used as **middlewares**. 4 |
5 |
6 | In **Middleware** can perform following tasks: 7 | - Execute any code. 8 | - change request body. 9 | - reply to request. 10 | - Call the next middleware function in the stack. 11 | 12 | If the current middleware function does not end the request-reply cycle, 13 | it must call next() to pass control to the next middleware function. 14 | Otherwise, the request will be left hanging. 15 | 16 | ### Examples 17 | 18 | ##### Simple usage of middlewares 19 | 20 | ```javascript 21 | import Node from 'zeronode' 22 | async function run() { 23 | try { 24 | let a = new Node() 25 | let b = new Node() 26 | await a.bind() 27 | await b.connect({ address: a.getAddress() }) 28 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 29 | conosle.log('In first middleware.') 30 | next() 31 | }) 32 | 33 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 34 | console.log('in second middleware.') 35 | reply() 36 | }) 37 | 38 | await b.request({ 39 | id: a.getId(), 40 | event: 'foo' 41 | }) 42 | 43 | console.log('done') 44 | } catch (err) { 45 | console.error(err) 46 | } 47 | } 48 | 49 | run() 50 | 51 | //after executing this code, it will print 52 | /* 53 | In first middleware. 54 | in second middleware. 55 | done 56 | */ 57 | 58 | ``` 59 | 60 |
61 | ##### Replying in First Middleware and calling next in second middleware 62 | 63 | ```javascript 64 | import Node from 'zeronode' 65 | async function run() { 66 | try { 67 | let a = new Node() 68 | let b = new Node() 69 | await a.bind() 70 | await b.connect({ address: a.getAddress() }) 71 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 72 | conosle.log('In first middleware.') 73 | reply() 74 | }) 75 | 76 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 77 | console.log('in second middleware.') 78 | next() 79 | }) 80 | 81 | await b.request({ 82 | id: a.getId(), 83 | event: 'foo' 84 | }) 85 | 86 | console.log('done') 87 | } catch (err) { 88 | console.error(err) 89 | } 90 | } 91 | 92 | run() 93 | 94 | //after executing this code, it will print 95 | /* 96 | In first middleware. 97 | done 98 | */ 99 | // The second middleware doesn't called, 100 | // because middlewares are called in same order as they added. 101 | ``` 102 | 103 |
104 | ##### Adding middlewares with regex 105 | 106 | ```javascript 107 | import Node from 'zeronode' 108 | async function run() { 109 | try { 110 | let a = new Node() 111 | let b = new Node() 112 | await a.bind() 113 | await b.connect({ address: a.getAddress() }) 114 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 115 | conosle.log('In first middleware.') 116 | next() 117 | }) 118 | 119 | a.onRequest(/^f/, ({ body, error, reply, next, head })) => { 120 | conosle.log('In second middleware.') 121 | next() 122 | }) 123 | 124 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 125 | console.log('in third middleware.') 126 | reply() 127 | }) 128 | 129 | await b.request({ 130 | id: a.getId(), 131 | event: 'foo' 132 | }) 133 | 134 | console.log('done') 135 | } catch (err) { 136 | console.error(err) 137 | } 138 | } 139 | 140 | run() 141 | 142 | //after executing this code, it will print 143 | /* 144 | In first middleware. 145 | in second middleware. 146 | in third middleware. 147 | done 148 | */ 149 | 150 | ``` 151 | 152 |
153 | ##### Changing body in middleware 154 | 155 | ```javascript 156 | import Node from 'zeronode' 157 | async function run() { 158 | try { 159 | let a = new Node() 160 | let b = new Node() 161 | await a.bind() 162 | await b.connect({ address: a.getAddress() }) 163 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 164 | conosle.log('In first middleware.', body.foo) 165 | body.foo = 'baz' 166 | next() 167 | }) 168 | 169 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 170 | console.log('in second middleware.', body.foo) 171 | reply() 172 | }) 173 | 174 | await b.request({ 175 | id: a.getId(), 176 | event: 'foo', 177 | data: { foo: 'bar' } 178 | }) 179 | 180 | console.log('done') 181 | } catch (err) { 182 | console.error(err) 183 | } 184 | } 185 | 186 | run() 187 | 188 | //after executing this code, it will print 189 | /* 190 | In first middleware. bar 191 | in second middleware. baz 192 | done 193 | */ 194 | 195 | ``` 196 | 197 |
198 | ##### Rejecting request 199 | 200 | ```javascript 201 | import Node from 'zeronode' 202 | async function run() { 203 | try { 204 | let a = new Node() 205 | let b = new Node() 206 | await a.bind() 207 | await b.connect({ address: a.getAddress() }) 208 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 209 | conosle.log('In first middleware.') 210 | next('error message.') 211 | }) 212 | 213 | a.onRequest('foo', ({ body, error, reply, next, head })) => { 214 | console.log('in second middleware.') 215 | reply() 216 | }) 217 | 218 | await b.request({ 219 | id: a.getId(), 220 | event: 'foo' 221 | }) 222 | 223 | console.log('done') 224 | } catch (err) { 225 | console.error(err) 226 | } 227 | } 228 | 229 | run() 230 | 231 | //after executing this code, it will print 232 | /* 233 | In first middleware. 234 | error message. 235 | */ 236 | 237 | ``` -------------------------------------------------------------------------------- /docs/TODO.md: -------------------------------------------------------------------------------- 1 | - // sockets/socket.js we need to add replyError () also add this in docs 2 | - // TODO::dhar maybe we need metrics by tags also under socket/socket.js 3 | - ~~// TODO::dhar check if all ticked events will reach clients even if we'll unbind quickly~~, (tested, it's ok) 4 | - // TODO::dhar optimize winner node, 5 | - // TODO::avar optimize node filtering(predicate), 6 | - // TODO::dhar send cpu/memory in pinging 7 | - // TODO::avar change monitor class, listening events 8 | - ~~// TODO::dhar separate custom events from main events~~ -------------------------------------------------------------------------------- /examples/node-cycle.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | import _ from 'underscore' 3 | 4 | 5 | (async function () { 6 | try { 7 | const NODE_COUNT = 10 8 | 9 | const MESSAGE_COUNT = 1000 10 | 11 | let count = 0 12 | 13 | let znodes = _.map(_.range(NODE_COUNT), (i) => { 14 | let znode = new Node() 15 | 16 | znode.onTick('foo', (msg) => { 17 | count++ 18 | 19 | if (count === MESSAGE_COUNT) { 20 | console.log('finished', count) 21 | return 22 | } 23 | 24 | znode.tickAny({ 25 | event: 'foo', 26 | data: `msg from znode${i}` 27 | }) 28 | }) 29 | 30 | return znode 31 | }) 32 | 33 | await Promise.all(_.map(znodes, async (znode, i) => { 34 | await znode.bind(`tcp://127.0.0.1:${3000 + i}`) 35 | if (i === 0) return 36 | await znode.connect({address: znodes[i - 1].getAddress()}) 37 | })) 38 | 39 | await znodes[0].connect({address: znodes[NODE_COUNT - 1].getAddress()}) 40 | 41 | znodes[0].tickAny({ 42 | event: 'foo', 43 | data: `msg from znode0` 44 | }) 45 | } catch (err) { 46 | console.error(err) 47 | } 48 | }()) 49 | -------------------------------------------------------------------------------- /examples/objectFilter.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | 4 | // znode1 5 | // /\ 6 | // / \ 7 | // / \ 8 | // znode2 znode3 9 | 10 | (async function () { 11 | let znode1 = new Node({ bind: 'tcp://127.0.0.1:3000' }) 12 | let znode2 = new Node({ options: { name: 'a' }}) 13 | let znode3 = new Node({ options: { name: 'b'}}) 14 | 15 | await znode1.bind() 16 | await znode2.connect({ address: znode1.getAddress() }) 17 | await znode3.connect({ address: znode1.getAddress() }) 18 | 19 | znode2.onTick('foo', (msg) => { 20 | console.log('handling tick on znode2:', msg) 21 | }) 22 | 23 | znode3.onTick('foo', (msg) => { 24 | console.log('handling tick on znode3:', msg) 25 | }) 26 | 27 | znode1.tickAll({ 28 | event: 'foo', 29 | data: 'tick from znode1.', 30 | filter: { 31 | name: 'a' 32 | } 33 | }) 34 | }()) -------------------------------------------------------------------------------- /examples/predicateFilter.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | import _ from 'underscore' 3 | 4 | // znode1 5 | // | 6 | // | 7 | // [clientNodes] 8 | // 9 | 10 | (async function () { 11 | let znode1 = new Node({ bind: 'tcp://127.0.0.1:3000' }) 12 | let clientNodes = _.map(_.range(10), (index) => { 13 | let znode = new Node({ options: { index } }) 14 | 15 | znode.onTick('foo', (msg) => { 16 | console.log(`handling tick on clienNode${index}:`, msg) 17 | }) 18 | 19 | return znode 20 | }) 21 | 22 | await znode1.bind() 23 | await Promise.all(_.map(clientNodes, (znode) => znode.connect({ address: znode1.getAddress() }))) 24 | 25 | znode1.tickAll({ 26 | event: 'foo', 27 | data: 'tick from znode1.', 28 | filter: (options) => options.index % 2 29 | }) 30 | }()) -------------------------------------------------------------------------------- /examples/regexpFilter.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | 4 | // znode1 5 | // /\ 6 | // / \ 7 | // / \ 8 | // znode2 znode3 9 | 10 | (async function () { 11 | let znode1 = new Node({ bind: 'tcp://127.0.0.1:3000' }) 12 | let znode2 = new Node({ options: { version: '1.2.4' }}) 13 | let znode3 = new Node({ options: { version: '0.0.6'}}) 14 | 15 | await znode1.bind() 16 | await znode2.connect({ address: znode1.getAddress() }) 17 | await znode3.connect({ address: znode1.getAddress() }) 18 | 19 | znode2.onTick('foo', (msg) => { 20 | console.log('handling tick on znode2:', msg) 21 | }) 22 | 23 | znode3.onTick('foo', (msg) => { 24 | console.log('handling tick on znode3:', msg) 25 | }) 26 | 27 | znode1.tickAll({ 28 | event: 'foo', 29 | data: 'tick from znode1.', 30 | filter: { 31 | version: /^1.(\d+\.)?(\d+)$/ 32 | } 33 | }) 34 | }()) -------------------------------------------------------------------------------- /examples/request-error.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | // znode1 4 | // | 5 | // | 6 | // znode2 7 | 8 | (async function () { 9 | try { 10 | let znode1 = new Node({bind: 'tcp://127.0.0.1:3000'}) 11 | let znode2 = new Node() 12 | 13 | await znode1.bind() 14 | await znode2.connect({ address: znode1.getAddress() }) 15 | 16 | znode1.onRequest('foo', (req) => { 17 | console.log('first handler:', req.body) 18 | req.body++ 19 | req.next() 20 | }) 21 | 22 | znode1.onRequest('foo', (req) => { 23 | console.log('second handler', req.body) 24 | req.body++ 25 | req.next('error message') 26 | }) 27 | 28 | znode1.onRequest('foo', (req) => { 29 | console.log('third handler', req.body) 30 | req.body++ 31 | req.reply(req.body) 32 | }) 33 | 34 | let rep = await znode2.request({ 35 | event: 'foo', 36 | to: znode1.getId(), 37 | data: 1 38 | }) 39 | 40 | console.log('reply', rep) 41 | } catch (err) { 42 | console.error('catching error:', err) 43 | } 44 | }()) -------------------------------------------------------------------------------- /examples/request-many-handlers.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | // znode1 4 | // | 5 | // | 6 | // znode2 7 | 8 | (async function () { 9 | let znode1 = new Node({bind: 'tcp://127.0.0.1:3000'}) 10 | let znode2 = new Node() 11 | 12 | await znode1.bind() 13 | await znode2.connect({ address: znode1.getAddress() }) 14 | 15 | znode1.onRequest('foo', (req) => { 16 | console.log('first handler:', req.body) 17 | req.body++ 18 | req.next() 19 | }) 20 | 21 | znode1.onRequest('foo', (req) => { 22 | console.log('second handler', req.body) 23 | req.body++ 24 | req.next() 25 | }) 26 | 27 | znode1.onRequest('foo', (req) => { 28 | console.log('third handler', req.body) 29 | req.body++ 30 | req.reply(req.body) 31 | }) 32 | 33 | let rep = await znode2.request({ 34 | event: 'foo', 35 | to: znode1.getId(), 36 | data: 1 37 | }) 38 | 39 | console.log('reply', rep) 40 | }()) -------------------------------------------------------------------------------- /examples/requestAny.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | // znode1 4 | // /\ 5 | // / \ 6 | // / \ 7 | // znode2 znode3 8 | 9 | (async function () { 10 | let znode1 = new Node({bind: 'tcp://127.0.0.1:3000'}) 11 | let znode2 = new Node() 12 | let znode3 = new Node() 13 | 14 | await znode1.bind() 15 | await znode2.connect({ address: znode1.getAddress() }) 16 | await znode3.connect({ address: znode1.getAddress() }) 17 | 18 | znode2.onRequest('foo', ({ body, reply }) => { 19 | console.log(body) 20 | reply('reply from znode2.') 21 | }) 22 | 23 | znode3.onRequest('foo', ({ body, reply }) => { 24 | console.log(body) 25 | reply('reply from znode3.') 26 | }) 27 | 28 | let rep = await znode1.requestAny({ 29 | event: 'foo', 30 | data: 'request from znode1.' 31 | }) 32 | 33 | console.log(rep) 34 | }()) -------------------------------------------------------------------------------- /examples/simple-request.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | // znode1 4 | // | 5 | // | 6 | // znode2 7 | 8 | (async function () { 9 | let znode1 = new Node({ bind: 'tcp://127.0.0.1:3000' }) 10 | let znode2 = new Node() 11 | 12 | await znode1.bind() 13 | await znode2.connect({ address: znode1.getAddress() }) 14 | 15 | znode1.onRequest('foo', ({ body, reply }) => { 16 | console.log(body) 17 | reply('reply from znode1.') 18 | }) 19 | 20 | let rep = await znode2.request({ 21 | event: 'foo', 22 | to: znode1.getId(), 23 | data: 'request from znode2.' 24 | }) 25 | 26 | console.log(rep) 27 | }()) -------------------------------------------------------------------------------- /examples/simple-tick.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | // znode1 4 | // | 5 | // | 6 | // znode2 7 | 8 | (async function () { 9 | let znode1 = new Node({bind: 'tcp://127.0.0.1:3000'}) 10 | let znode2 = new Node() 11 | 12 | await znode1.bind() 13 | await znode2.connect({ address: znode1.getAddress() }) 14 | 15 | znode1.onTick('foo', (msg) => { 16 | console.log(msg) 17 | }) 18 | 19 | znode2.tick({ 20 | event: 'foo', 21 | to: znode1.getId(), 22 | data: 'msg from znode2' 23 | }) 24 | }()) -------------------------------------------------------------------------------- /examples/tickAll.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | // znode1 4 | // | 5 | // | 6 | // znode2 7 | // /\ 8 | // / \ 9 | // / \ 10 | // znode3 znode4 11 | (async function () { 12 | let znode1 = new Node({ bind: 'tcp://127.0.0.1:3000' }) 13 | let znode2 = new Node({ bind: 'tcp://127.0.0.1:3001' }) 14 | let znode3 = new Node() 15 | let znode4 = new Node() 16 | 17 | await znode1.bind() 18 | await znode2.bind() 19 | await znode2.connect({ address: znode1.getAddress() }) 20 | await znode3.connect({ address: znode2.getAddress() }) 21 | await znode4.connect({ address: znode2.getAddress() }) 22 | 23 | znode1.onTick('foo', (msg) => { 24 | console.log('handling tick on znode1:', msg) 25 | }) 26 | znode2.onTick('foo', (msg) => { 27 | console.log('handling tick on znode2:', msg) 28 | }) 29 | znode3.onTick('foo', (msg) => { 30 | console.log('handling tick on znode3:', msg) 31 | }) 32 | znode4.onTick('foo', (msg) => { 33 | console.log('handling tick on znode4:', msg) 34 | }) 35 | 36 | 37 | znode2.tickAll({ 38 | event: 'foo', 39 | data: 'msg from znode2' 40 | }) 41 | }()) -------------------------------------------------------------------------------- /examples/tickAny.js: -------------------------------------------------------------------------------- 1 | import { Node } from '../src' 2 | 3 | 4 | // znode1 5 | // /\ 6 | // / \ 7 | // / \ 8 | // znode2 znode3 9 | 10 | (async function () { 11 | let znode1 = new Node({bind: 'tcp://127.0.0.1:3000'}) 12 | let znode2 = new Node() 13 | let znode3 = new Node() 14 | 15 | await znode1.bind() 16 | await znode2.connect({ address: znode1.getAddress() }) 17 | await znode3.connect({ address: znode1.getAddress() }) 18 | 19 | znode2.onTick('foo', (msg) => { 20 | console.log('handling tick on znode2:', msg) 21 | }) 22 | 23 | znode3.onTick('foo', (msg) => { 24 | console.log('handling tick on znode3:', msg) 25 | }) 26 | 27 | znode1.tickAny({ 28 | event: 'foo', 29 | data: 'tick from znode1.' 30 | }) 31 | }()) -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "zeronode", 3 | "version": "1.1.35", 4 | "description": "Minimal building block for NodeJS microservices", 5 | "main": "./dist/index.js", 6 | "directories": { 7 | "example": "examples" 8 | }, 9 | "keywords": [ 10 | "micro", 11 | "service", 12 | "microservice", 13 | "micro-service", 14 | "microservices", 15 | "micro-services", 16 | "services", 17 | "micro services", 18 | "micro service", 19 | "networking", 20 | "distributed", 21 | "distributed-message", 22 | "distributed message", 23 | "loadbalancing", 24 | "loadbalance", 25 | "request" 26 | ], 27 | "scripts": { 28 | "test": "cross-env NODE_ENV=test nyc --check-coverage mocha --exit --timeout 10000", 29 | "snyktest": "snyk test", 30 | "standard": "standard './src/**/*.js' --parser babel-eslint --verbose | snazzy", 31 | "format": "standard './src/**/*.js' --parser babel-eslint --fix --verbose | snazzy", 32 | "rimraf": "rimraf", 33 | "clear": "rimraf ./dist", 34 | "compile": "./node_modules/.bin/babel -d dist/ src/", 35 | "build": "npm run clear && npm run compile", 36 | "preinstall": "bash preinstall.sh", 37 | "snyk-protect": "snyk protect", 38 | "prepare": "npm run build && npm run snyk-protect" 39 | }, 40 | "repository": { 41 | "type": "git", 42 | "url": "git+https://github.com/sfast/zeronode.git" 43 | }, 44 | "author": "Steadfast.tech", 45 | "license": "MIT", 46 | "bugs": { 47 | "url": "https://github.com/sfast/zeronode/issues" 48 | }, 49 | "homepage": "https://github.com/sfast/zeronode#readme", 50 | "dependencies": { 51 | "@babel/runtime": "^7.5.5", 52 | "animal-id": "0.0.1", 53 | "bluebird": "^3.5.5", 54 | "buffer-alloc": "^1.2.0", 55 | "buffer-from": "^1.1.1", 56 | "lokijs": "^1.5.7", 57 | "md5": "^2.2.1", 58 | "pattern-emitter": "latest", 59 | "underscore": "^1.9.1", 60 | "uuid": "^3.3.2", 61 | "winston": "^3.2.1", 62 | "zeromq": "4.6.0" 63 | }, 64 | "devDependencies": { 65 | "@babel/cli": "^7.5.5", 66 | "@babel/core": "^7.5.5", 67 | "@babel/node": "^7.5.5", 68 | "@babel/plugin-proposal-function-bind": "^7.2.0", 69 | "@babel/plugin-proposal-object-rest-spread": "^7.5.5", 70 | "@babel/plugin-transform-runtime": "^7.5.5", 71 | "@babel/preset-env": "^7.5.5", 72 | "@babel/register": "^7.5.5", 73 | "babel-eslint": "^10.0.2", 74 | "babel-plugin-istanbul": "^5.2.0", 75 | "chai": "^4.2.0", 76 | "cross-env": "^5.2.0", 77 | "eslint-plugin-import": "^2.18.2", 78 | "eslint-plugin-node": "^9.1.0", 79 | "eslint-plugin-promise": "^4.2.1", 80 | "js-yaml": "^3.13.1", 81 | "mocha": "^6.2.0", 82 | "nyc": "^14.1.1", 83 | "rimraf": "^2.6.3", 84 | "seneca": "^3.13.2", 85 | "snazzy": "^8.0.0", 86 | "snyk": "^1.216.1", 87 | "standard": "^13.1.0" 88 | }, 89 | "snyk": true, 90 | "nyc": { 91 | "require": [ 92 | "@babel/register" 93 | ], 94 | "reporter": [ 95 | "lcov", 96 | "text" 97 | ], 98 | "sourceMap": false, 99 | "instrument": false 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /preinstall.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | lowercase(){ 4 | echo "$1" | sed "y/ABCDEFGHIJKLMNOPQRSTUVWXYZ/abcdefghijklmnopqrstuvwxyz/" 5 | } 6 | 7 | OS=`lowercase \`uname\`` 8 | KERNEL=`uname -r` 9 | MACH=`uname -m` 10 | 11 | packageManager="sudo apt-get" 12 | libzmq="libzmq3-dev" 13 | 14 | if [ "${OS}" == "windowsnt" ]; then 15 | OS=windows 16 | elif [ "${OS}" == "darwin" ]; then 17 | OS=mac 18 | packageManager="brew" 19 | libzmq="zeromq" 20 | else 21 | OS=`uname` 22 | if [ "${OS}" = "SunOS" ] ; then 23 | OS=Solaris 24 | ARCH=`uname -p` 25 | OSSTR="${OS} {$REV}(${ARCH} `uname -v`)" 26 | elif [ "${OS}" = "AIX" ] ; then 27 | packageManager="smit" 28 | OSSTR="${OS} `oslevel` (`oslevel -r`)" 29 | elif [ "${OS}" = "Linux" ] ; then 30 | if [ -f /etc/redhat-release ] ; then 31 | DistroBasedOn='RedHat' 32 | packageManager="sudo yum" 33 | libzmq="zeromq" 34 | DIST=`cat /etc/redhat-release |sed s/\ release.*//` 35 | PSUEDONAME=`cat /etc/redhat-release | sed s/.*\(// | sed s/\)//` 36 | REV=`cat /etc/redhat-release | sed s/.*release\ // | sed s/\ .*//` 37 | elif [ -f /etc/SuSE-release ] ; then 38 | DistroBasedOn='SuSe' 39 | packageManager="zypper" 40 | libzmq="zeromq" 41 | PSUEDONAME=`cat /etc/SuSE-release | tr "\n" ' '| sed s/VERSION.*//` 42 | REV=`cat /etc/SuSE-release | tr "\n" ' ' | sed s/.*=\ //` 43 | elif [ -f /etc/mandrake-release ] ; then 44 | DistroBasedOn='Mandrake' 45 | PSUEDONAME=`cat /etc/mandrake-release | sed s/.*\(// | sed s/\)//` 46 | REV=`cat /etc/mandrake-release | sed s/.*release\ // | sed s/\ .*//` 47 | elif [ -f /etc/debian_version ] && [ -f /etc/lsb-release ] ; then 48 | DistroBasedOn='Debian' 49 | DIST=`cat /etc/lsb-release | grep '^DISTRIB_ID' | awk -F= '{ print $2 }'` 50 | PSUEDONAME=`cat /etc/lsb-release | grep '^DISTRIB_CODENAME' | awk -F= '{ print $2 }'` 51 | REV=`cat /etc/lsb-release | grep '^DISTRIB_RELEASE' | awk -F= '{ print $2 }'` 52 | elif [ -f /sys/hypervisor/uuid ] && [ `head -c 3 /sys/hypervisor/uuid` == ec2 ]; then 53 | DistroBasedOn='RedHat' 54 | packageManager="sudo yum" 55 | libzmq="zeromq" 56 | fi 57 | if [ -f /etc/UnitedLinux-release ] ; then 58 | DIST="${DIST}[`cat /etc/UnitedLinux-release | tr "\n" ' ' | sed s/VERSION.*//`]" 59 | fi 60 | OS=`lowercase $OS` 61 | DistroBasedOn=`lowercase $DistroBasedOn` 62 | readonly DIST 63 | readonly DistroBasedOn 64 | readonly PSUEDONAME 65 | readonly REV 66 | readonly KERNEL 67 | readonly MACH 68 | fi 69 | 70 | fi 71 | 72 | if [ "${OS}" != "mac" ] && [ "${DistroBasedOn}" != "debian" ] && [ "${DistroBasedOn}" != "redhat" ]; then 73 | echo "Can't install zeromq on this os, need to install manually." 74 | exit 0 75 | fi 76 | 77 | if [ -z "$(which pkg-config)" ]; then 78 | $packageManager install pkg-config 79 | fi 80 | 81 | pkg-config libzmq --exists 82 | 83 | haveZmq=$? 84 | 85 | if [ $haveZmq == 0 ]; then 86 | exit 0; 87 | fi 88 | 89 | if [ $packageManager == "brew" ]; then 90 | $packageManager install $libzmq 91 | else 92 | $packageManager install -y $libzmq 93 | fi 94 | -------------------------------------------------------------------------------- /src/actor.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by artak on 3/2/17. 3 | */ 4 | 5 | // ** ActorModel is a general model for describing both client and server nodes/actors 6 | 7 | export default class ActorModel { 8 | constructor (data = {}) { 9 | let { id, online = true, address, options } = data 10 | this.id = id 11 | 12 | this.online = false 13 | 14 | if (online) { 15 | this.setOnline() 16 | } 17 | 18 | this.address = address 19 | this.options = options || {} 20 | 21 | this.pingStamp = null 22 | this.ghost = false 23 | this.fail = false 24 | this.stop = false 25 | } 26 | 27 | toJSON () { 28 | return { 29 | id: this.id, 30 | address: this.address, 31 | options: this.options, 32 | fail: this.fail, 33 | stop: this.stop, 34 | online: this.online, 35 | ghost: this.ghost 36 | } 37 | } 38 | 39 | getId () { 40 | return this.id 41 | } 42 | 43 | markStopped () { 44 | this.stop = Date.now() 45 | this.setOffline() 46 | } 47 | 48 | markFailed () { 49 | this.fail = Date.now() 50 | this.setOffline() 51 | } 52 | 53 | // ** marking ghost means that there was some ping delay but that doeas not actually mean that its not there 54 | markGhost () { 55 | this.ghost = Date.now() 56 | } 57 | 58 | isGhost () { 59 | return !!this.ghost 60 | } 61 | 62 | isOnline () { 63 | return !!this.online 64 | } 65 | 66 | setOnline () { 67 | this.online = Date.now() 68 | this.ghost = false 69 | this.fail = false 70 | this.stop = false 71 | } 72 | 73 | setOffline () { 74 | this.online = false 75 | } 76 | 77 | ping (stamp) { 78 | this.pingStamp = stamp 79 | this.setOnline() 80 | } 81 | 82 | setId (newId) { 83 | this.id = newId 84 | } 85 | 86 | setAddress (address) { 87 | this.address = address 88 | } 89 | 90 | getAddress () { 91 | return this.address 92 | } 93 | 94 | setOptions (options) { 95 | this.options = options 96 | } 97 | 98 | mergeOptions (options) { 99 | this.options = Object.assign({}, this.options, options) 100 | return this.options 101 | } 102 | 103 | getOptions () { 104 | return this.options 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/client.js: -------------------------------------------------------------------------------- 1 | import { events } from './enum' 2 | import Globals from './globals' 3 | import ActorModel from './actor' 4 | import { ZeronodeError, ErrorCodes } from './errors' 5 | 6 | import { Dealer as DealerSocket, SocketEvent } from './sockets' 7 | 8 | let _private = new WeakMap() 9 | 10 | export default class Client extends DealerSocket { 11 | constructor ({ id, options, config } = {}) { 12 | options = options || {} 13 | config = config || {} 14 | 15 | super({ id, options, config }) 16 | let _scope = { 17 | server: null, 18 | pingInterval: null 19 | } 20 | 21 | this.on(SocketEvent.DISCONNECT, this::_serverFailHandler) 22 | this.on(SocketEvent.RECONNECT, this::_serverReconnectHandler) 23 | this.on(SocketEvent.RECONNECT_FAILURE, () => this.emit(events.SERVER_RECONNECT_FAILURE, _scope.server.toJSON())) 24 | 25 | this.onTick(events.SERVER_STOP, this::_serverStopHandler, true) 26 | this.onTick(events.OPTIONS_SYNC, this::_serverOptionsSync, true) 27 | 28 | _private.set(this, _scope) 29 | } 30 | 31 | getServerActor () { 32 | let { server } = _private.get(this) 33 | return server 34 | } 35 | 36 | setOptions (options, notify = true) { 37 | super.setOptions(options) 38 | if (notify) { 39 | this.tick({ event: events.OPTIONS_SYNC, data: { actorId: this.getId(), options }, mainEvent: true }) 40 | } 41 | } 42 | 43 | // ** returns a promise which resolves with server model after server replies to events.CLIENT_CONNECTED 44 | async connect (serverAddress, timeout) { 45 | try { 46 | let _scope = _private.get(this) 47 | 48 | // actually connected 49 | await super.connect(serverAddress, timeout) 50 | 51 | let requestData = { 52 | event: events.CLIENT_CONNECTED, 53 | data: { 54 | actorId: this.getId(), 55 | options: this.getOptions() 56 | }, 57 | mainEvent: true 58 | } 59 | 60 | let { actorId, options } = await this.request(requestData) 61 | // ** creating server model and setting it online 62 | _scope.server = new ActorModel({ id: actorId, options: options, online: true, address: serverAddress }) 63 | this::_startServerPinging() 64 | return { actorId, options } 65 | } catch (err) { 66 | let clientConnectError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.CLIENT_CONNECT, error: err }) 67 | clientConnectError.description = `Error while disconnecting client '${this.getId()}'` 68 | this.emit('error', clientConnectError) 69 | } 70 | } 71 | 72 | async disconnect (options) { 73 | try { 74 | let _scope = _private.get(this) 75 | let server = this.getServerActor() 76 | let disconnectData = { actorId: this.getId() } 77 | 78 | if (options) { 79 | disconnectData.options = options 80 | } 81 | 82 | if (server && server.isOnline()) { 83 | let requestOb = { 84 | event: events.CLIENT_STOP, 85 | data: disconnectData, 86 | mainEvent: true 87 | } 88 | 89 | await this.request(requestOb) 90 | _scope.server = null 91 | } 92 | 93 | this::_stopServerPinging() 94 | 95 | super.disconnect() 96 | } catch (err) { 97 | let clientDisconnectError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.CLIENT_DISCONNECT, error: err }) 98 | clientDisconnectError.description = `Error while disconnecting client '${this.getId()}'` 99 | this.emit('error', clientDisconnectError) 100 | } 101 | } 102 | 103 | request ({ event, data, timeout, mainEvent } = {}) { 104 | let server = this.getServerActor() 105 | 106 | // this is first request, and there is no need to check if server online or not 107 | if (mainEvent && event === events.CLIENT_CONNECTED) { 108 | return super.request({ event, data, timeout, mainEvent }) 109 | } 110 | 111 | if (!server || !server.isOnline()) { 112 | let serverOfflineError = new Error(`Server is offline during request, on client: ${this.getId()}`) 113 | return Promise.reject(new ZeronodeError({ socketId: this.getId(), error: serverOfflineError, code: ErrorCodes.SERVER_IS_OFFLINE })) 114 | } 115 | 116 | return super.request({ event, data, timeout, to: server.getId(), mainEvent }) 117 | } 118 | 119 | tick ({ event, data, mainEvent } = {}) { 120 | let server = this.getServerActor() 121 | 122 | if (!server || !server.isOnline()) { 123 | let serverOfflineError = new Error(`Server is offline during request, on client: ${this.getId()}`) 124 | return Promise.reject(new ZeronodeError({ socketId: this.getId(), error: serverOfflineError, code: ErrorCodes.SERVER_IS_OFFLINE })) 125 | } 126 | 127 | super.tick({ event, data, to: server.getId(), mainEvent }) 128 | } 129 | } 130 | 131 | function _serverFailHandler () { 132 | try { 133 | let server = this.getServerActor() 134 | 135 | if (!server || !server.isOnline()) return 136 | 137 | this::_stopServerPinging() 138 | 139 | server.markFailed() 140 | 141 | this.emit(events.SERVER_FAILURE, server.toJSON()) 142 | } catch (err) { 143 | let serverFailHandlerError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.SERVER_RECONNECT_HANDLER, error: err }) 144 | serverFailHandlerError.description = `Error while handling server failure on client ${this.getId()}` 145 | this.emit('error', serverFailHandlerError) 146 | } 147 | } 148 | 149 | async function _serverReconnectHandler (/* { fd, serverAddress } */) { 150 | try { 151 | let server = this.getServerActor() 152 | 153 | let requestObj = { 154 | event: events.CLIENT_CONNECTED, 155 | data: { 156 | actorId: this.getId(), 157 | options: this.getOptions() 158 | }, 159 | mainEvent: true 160 | } 161 | 162 | let { actorId, options } = await this.request(requestObj) 163 | 164 | // ** TODO։։avar remove this after some time (server should always be available at this point) 165 | if (!server) { 166 | throw new Error(`Server actor is not available on client '${this.getId()}'`) 167 | } 168 | 169 | server.setId(actorId) 170 | server.setOnline() 171 | server.setOptions(options) 172 | 173 | this.emit(events.SERVER_RECONNECT, server.toJSON()) 174 | 175 | this::_startServerPinging() 176 | } catch (err) { 177 | let serverReconnectHandlerError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.SERVER_RECONNECT_HANDLER, error: err }) 178 | serverReconnectHandlerError.description = `Error while handling server reconnect on client ${this.getId()}` 179 | this.emit('error', serverReconnectHandlerError) 180 | } 181 | } 182 | 183 | function _serverStopHandler () { 184 | try { 185 | let server = this.getServerActor() 186 | 187 | // ** TODO:: this should not happen, please describe the situation 188 | if (!server) { 189 | throw new Error(`Server actor is not available on client '${this.getId()}'`) 190 | } 191 | 192 | this::_stopServerPinging() 193 | 194 | server.markStopped() 195 | this.emit(events.SERVER_STOP, server.toJSON()) 196 | } catch (err) { 197 | let serverStopHandlerError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.SERVER_STOP_HANDLER, error: err }) 198 | serverStopHandlerError.description = `Error while handling server stop on client ${this.getId()}` 199 | this.emit('error', serverStopHandlerError) 200 | } 201 | } 202 | 203 | function _serverOptionsSync ({ options, actorId }) { 204 | try { 205 | let server = this.getServerActor() 206 | if (!server) { 207 | throw new Error(`Server actor is not available on client '${this.getId()}'`) 208 | } 209 | server.setOptions(options) 210 | this.emit(events.OPTIONS_SYNC, { id: server.getId(), newOptions: options }) 211 | } catch (err) { 212 | let serverOptionsSyncHandlerError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.SERVER_OPTIONS_SYNC_HANDLER, error: err }) 213 | serverOptionsSyncHandlerError.description = `Error while handling server options sync on client ${this.getId()}` 214 | this.emit('error', serverOptionsSyncHandlerError) 215 | } 216 | } 217 | 218 | function _startServerPinging () { 219 | let _scope = _private.get(this) 220 | let { pingInterval } = _scope 221 | 222 | if (pingInterval) { 223 | clearInterval(pingInterval) 224 | } 225 | 226 | let config = this.getConfig() 227 | let interval = config.CLIENT_PING_INTERVAL || Globals.CLIENT_PING_INTERVAL 228 | 229 | _scope.pingInterval = setInterval(() => { 230 | try { 231 | let pingData = { actor: this.getId(), stamp: Date.now() } 232 | this.tick({ event: events.CLIENT_PING, data: pingData, mainEvent: true }) 233 | } catch (err) { 234 | let pingError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.SERVER_PING_ERROR, error: err }) 235 | this.emit('error', pingError) 236 | } 237 | }, interval) 238 | } 239 | 240 | function _stopServerPinging () { 241 | let { pingInterval } = _private.get(this) 242 | 243 | if (pingInterval) { 244 | clearInterval(pingInterval) 245 | } 246 | } 247 | -------------------------------------------------------------------------------- /src/enum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by artak on 2/15/17. 3 | */ 4 | 5 | export const events = { 6 | CLIENT_CONNECTED: 1, 7 | CLIENT_FAILURE: 2, 8 | CLIENT_STOP: 3, 9 | CLIENT_PING: 4, 10 | OPTIONS_SYNC: 5, 11 | SERVER_RECONNECT: 6, 12 | SERVER_FAILURE: 7, 13 | SERVER_STOP: 8, 14 | METRICS: 9, 15 | SERVER_RECONNECT_FAILURE: 10, 16 | CONNECT_TO_SERVER: 11 17 | } 18 | 19 | export const MetricCollections = { 20 | SEND_REQUEST: 'send_request', 21 | SEND_TICK: 'send_tick', 22 | GOT_REQUEST: 'got_request', 23 | GOT_TICK: 'got_tick', 24 | AGGREGATION: 'aggregation' 25 | } 26 | -------------------------------------------------------------------------------- /src/errors.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dave on 7/11/17. 3 | */ 4 | 5 | const ErrorCodes = { 6 | ALREADY_BINDED: 1, 7 | SOCKET_ISNOT_ONLINE: 2, 8 | NO_NEXT_HANDLER_AVAILABLE: 3, 9 | REQUEST_TIMEOUTED: 4, 10 | ALREADY_CONNECTED: 5, 11 | CONNECTION_TIMEOUT: 6, 12 | SERVER_UNBIND: 7, 13 | SERVER_ACTOR_NOT_AVAILABLE: 8, 14 | SERVER_STOP_HANDLER: 9, 15 | SERVER_OPTIONS_SYNC_HANDLER: 10, 16 | SERVER_PING_ERROR: 11, 17 | CLIENT_OPTIONS_SYNC_HANDLER: 12, 18 | SERVER_RECONNECT_HANDLER: 13, 19 | NODE_NOT_FOUND: 14, 20 | CLIENT_DISCONNECT: 15, 21 | CLIENT_CONNECT: 16, 22 | SERVER_IS_OFFLINE: 17 23 | } 24 | 25 | class ZeronodeError extends Error { 26 | constructor ({ socketId, envelopId, code, error, message, description } = {}) { 27 | error = error || {} 28 | message = message || error.message 29 | description = description || message 30 | super(message) 31 | this.socketId = socketId 32 | this.code = code 33 | this.envelopId = envelopId 34 | this.error = error 35 | this.description = description 36 | } 37 | } 38 | 39 | export { ZeronodeError } 40 | export { ErrorCodes } 41 | 42 | export default { 43 | ErrorCodes, 44 | ZeronodeError 45 | } 46 | -------------------------------------------------------------------------------- /src/globals.js: -------------------------------------------------------------------------------- 1 | export default { 2 | CLIENT_MUST_HEARTBEAT_INTERVAL: 6000, 3 | CLIENT_PING_INTERVAL: 2000 4 | } 5 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by root on 7/11/17. 3 | */ 4 | import Node from './node' 5 | import { events as NodeEvents, MetricCollections } from './enum' 6 | import { ErrorCodes } from './errors' 7 | import Server from './server' 8 | import Client from './client' 9 | import { Enum } from './sockets' 10 | 11 | let MetricEvents = Enum.MetricType 12 | 13 | export { Node, Server, Client, NodeEvents, ErrorCodes, MetricEvents, MetricCollections } 14 | 15 | export default Node 16 | -------------------------------------------------------------------------------- /src/metric.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by dhar on 7/12/17. 3 | */ 4 | 5 | import Loki from 'lokijs' 6 | import _ from 'underscore' 7 | import { MetricCollections } from './enum' 8 | 9 | const truePredicate = () => true 10 | 11 | const finishedPredicate = (req) => { 12 | return req.success || req.error || req.timeout 13 | } 14 | 15 | const averageCalc = (a, n, b, m) => a * (n / (n + m)) + b * (m / (n + m)) 16 | 17 | const MetricUtils = { 18 | createRequest: (envelop) => { 19 | return { 20 | id: envelop.id, 21 | event: envelop.tag, 22 | from: envelop.owner, 23 | to: envelop.recipient, 24 | size: [envelop.size], 25 | timeout: false, 26 | duration: { 27 | latency: -1, 28 | process: -1 29 | }, 30 | success: false, 31 | error: false 32 | } 33 | }, 34 | createTick: (envelop) => { 35 | return { 36 | id: envelop.id, 37 | event: envelop.tag, 38 | from: envelop.owner, 39 | to: envelop.recipient, 40 | size: envelop.size 41 | } 42 | } 43 | } 44 | 45 | let _private = new WeakMap() 46 | 47 | const _updateAggregationTable = function () { 48 | let _scope = _private.get(this) 49 | 50 | let { aggregationTable, customColumns } = _scope 51 | // resetting timeout and count 52 | _scope.count = 0 53 | clearTimeout(_scope.flushTimeoutInstance) 54 | _scope.flushTimeoutInstance = setTimeout(this::_updateAggregationTable, _scope.flushTimeout) 55 | 56 | // getting requests and ticks 57 | let sendRequests = _scope.sendRequestCollection.where(finishedPredicate) 58 | let gotRequests = _scope.gotRequestCollection.where(finishedPredicate) 59 | let sendTicks = _scope.sendTickCollection.where(truePredicate) 60 | let gotTicks = _scope.gotTickCollection.where(truePredicate) 61 | 62 | // grouping by node and event 63 | sendRequests = _.groupBy(sendRequests, (request) => `${request.to}${request.event}`) 64 | gotRequests = _.groupBy(gotRequests, (request) => `${request.from}${request.event}`) 65 | sendTicks = _.groupBy(sendTicks, (tick) => `${tick.to}${tick.event}`) 66 | gotTicks = _.groupBy(gotTicks, (tick) => `${tick.from}${tick.event}`) 67 | 68 | // updating row in aggregation table 69 | const updateRequestRow = (node, event, groupedRequests, out = false) => { 70 | let row = aggregationTable.findOne({ node, event, out, request: true }) 71 | 72 | if (!row) { 73 | row = { 74 | node, 75 | event, 76 | out, 77 | request: true, 78 | latency: 0, 79 | process: 0, 80 | count: 0, 81 | success: 0, 82 | error: 0, 83 | timeout: 0, 84 | size: 0 85 | } 86 | _.each(customColumns, ({ initialValue }, columnName) => { 87 | row[columnName] = initialValue 88 | }) 89 | row = aggregationTable.insert(row) 90 | } 91 | 92 | let latencySum = 0 93 | let processSum = 0 94 | let sizeSum = 0 95 | let allCount = row.count 96 | let initialCount = row.count - row.timeout 97 | 98 | _.each(groupedRequests, (request) => { 99 | row.count++ 100 | row.success += request.success 101 | row.error += request.error 102 | row.timeout += request.timeout 103 | latencySum += request.duration.latency 104 | processSum += request.duration.process 105 | sizeSum += request.size[0] + request.size[1] 106 | _.each(customColumns, ({ reducer }, columnName) => { 107 | row[columnName] = reducer(row, request) 108 | }) 109 | }) 110 | 111 | row.latency = averageCalc(row.latency, initialCount, latencySum / (row.count - initialCount - row.timeout), row.count - initialCount - row.timeout) 112 | row.process = averageCalc(row.process, initialCount, processSum / (row.count - initialCount - row.timeout), row.count - initialCount - row.timeout) 113 | row.size = averageCalc(row.size, allCount, sizeSum / (row.count - allCount), row.count - allCount) 114 | 115 | aggregationTable.update(row) 116 | } 117 | 118 | // updating row in aggregation table 119 | const updateTickRow = (node, event, groupedTicks, out = false) => { 120 | let row = aggregationTable.find({ node, event, out, request: false }) 121 | if (!row) { 122 | row = { 123 | node, 124 | event, 125 | out, 126 | request: false, 127 | count: 0, 128 | size: 0 129 | } 130 | _.each(customColumns, ({ initialValue }, columnName) => { 131 | row[columnName] = initialValue 132 | }) 133 | row = aggregationTable.insert(row) 134 | } 135 | 136 | let sizeSum = 0 137 | let initialCount = row.count 138 | 139 | _.each(groupedTicks, (request) => { 140 | row.count++ 141 | sizeSum += request.size 142 | _.each(customColumns, ({ reducer }, columnName) => { 143 | row[columnName] = reducer(row, request) 144 | }) 145 | }) 146 | 147 | row.size = averageCalc(row.size, initialCount, sizeSum / groupedTicks.length, groupedTicks.length) 148 | 149 | aggregationTable.update(row) 150 | } 151 | 152 | _.each(sendRequests, (groupedRequests) => { 153 | updateRequestRow(groupedRequests[0].to, groupedRequests[0].event, groupedRequests, true) 154 | }) 155 | _.each(gotRequests, (groupedRequests) => { 156 | updateRequestRow(groupedRequests[0].from, groupedRequests[0].event, groupedRequests) 157 | }) 158 | _.each(sendTicks, (groupedTicks) => { 159 | updateTickRow(groupedTicks[0].from, groupedTicks[0].event, groupedTicks, true) 160 | }) 161 | _.each(gotTicks, (groupedTicks) => { 162 | updateTickRow(groupedTicks[0].from, groupedTicks[0].event, groupedTicks) 163 | }) 164 | 165 | this.flush() 166 | } 167 | 168 | export default class Metric { 169 | constructor ({ id } = {}) { 170 | let ZeronodeMetricDB = new Loki('zeronode.db') 171 | 172 | let _scope = { 173 | id, 174 | enabled: false, 175 | // ** loki collections 176 | sendRequestCollection: ZeronodeMetricDB.addCollection(MetricCollections.SEND_REQUEST, { indices: ['id'] }), 177 | gotRequestCollection: ZeronodeMetricDB.addCollection(MetricCollections.GOT_REQUEST, { indices: ['id'] }), 178 | sendTickCollection: ZeronodeMetricDB.addCollection(MetricCollections.SEND_TICK, { indices: ['id'] }), 179 | gotTickCollection: ZeronodeMetricDB.addCollection(MetricCollections.GOT_TICK, { indices: ['id'] }), 180 | aggregationTable: ZeronodeMetricDB.addCollection(MetricCollections.AGGREGATION, { indices: ['node', 'event'] }), 181 | flushTimeoutInstance: null, 182 | flushTimeout: 30 * 1000, 183 | customColumns: {}, 184 | count: 0 185 | } 186 | _private.set(this, _scope) 187 | this.db = ZeronodeMetricDB 188 | } 189 | 190 | get status () { 191 | let { enabled } = _private.get(this) 192 | return enabled 193 | } 194 | 195 | getMetrics (query = {}) { 196 | let { aggregationTable } = _private.get(this) 197 | if (!this.status) return 198 | let result = aggregationTable.find(query) 199 | 200 | let total = { 201 | count: 0, 202 | latency: 0, 203 | process: 0, 204 | out: 0, 205 | in: 0, 206 | request: 0, 207 | tick: 0, 208 | error: 0, 209 | success: 0, 210 | timeout: 0, 211 | size: 0 212 | } 213 | 214 | // calculating total 215 | total = _.reduce(result, (memo, row) => { 216 | let initialCount = memo.count 217 | let initialOut = memo.out 218 | let initialTimeout = memo.timeout 219 | memo.count += row.count 220 | row.out ? memo.out += row.count : memo.in += row.count 221 | row.request ? memo.request += row.count : memo.tick += row.count 222 | 223 | if (row.request) { 224 | memo.error += row.error 225 | memo.success += row.success 226 | memo.timeout += row.timeout 227 | 228 | if (row.out) { 229 | memo.latency = averageCalc(memo.latency, initialOut - initialTimeout, row.latency, row.count - row.timeout) 230 | memo.process = averageCalc(memo.process, initialOut - initialTimeout, row.process, row.count - row.timeout) 231 | } 232 | } 233 | 234 | memo.size = averageCalc(memo.size, initialCount, row.size, row.count) 235 | return memo 236 | }, total) 237 | 238 | return { result, total } 239 | } 240 | 241 | defineColumn (columnName, initialValue, reducer, isIndex = false) { 242 | let { aggregationTable, customColumns } = _private.get(this) 243 | if (this.status) throw new Error(`Can't define column after metrics enabled`) 244 | if (isIndex) { 245 | aggregationTable.ensureIndex(columnName) 246 | } 247 | 248 | customColumns[columnName] = { initialValue, reducer, isIndex } 249 | } 250 | 251 | 252 | //TODO:: avar, dave 253 | enable (flushTimeout) { 254 | let _scope = _private.get(this) 255 | _scope.enabled = true 256 | _scope.flushTimeout = flushTimeout || _scope.flushTimeout 257 | _scope.flushTimeoutInstance = setTimeout(this::_updateAggregationTable, _scope.flushTimeout) 258 | } 259 | 260 | //TODO:: avar, dave 261 | disable () { 262 | let _scope = _private.get(this) 263 | _scope.enabled = false 264 | clearTimeout(_scope.flushTimeoutInstance) 265 | this::_updateAggregationTable() 266 | _scope.count = 0 267 | } 268 | 269 | // ** actions 270 | sendRequest (envelop) { 271 | let { sendRequestCollection, enabled } = _private.get(this) 272 | if (!enabled) return 273 | let requestInstance = MetricUtils.createRequest(envelop) 274 | sendRequestCollection.insert(requestInstance) 275 | } 276 | 277 | gotRequest (envelop) { 278 | let { gotRequestCollection, enabled } = _private.get(this) 279 | if (!enabled) return 280 | let requestInstance = MetricUtils.createRequest(envelop) 281 | gotRequestCollection.insert(requestInstance) 282 | } 283 | 284 | sendReplySuccess (envelop) { 285 | let _scope = _private.get(this) 286 | let { gotRequestCollection, enabled } = _scope 287 | if (!enabled) return 288 | let request = gotRequestCollection.findOne({ id: envelop.id }) 289 | 290 | if (!request) return 291 | 292 | request.success = true 293 | request.size.push(envelop.size) 294 | 295 | gotRequestCollection.update(request) 296 | 297 | if (++_scope.count === 1000) { 298 | this::_updateAggregationTable() 299 | } 300 | } 301 | 302 | sendReplyError (envelop) { 303 | let _scope = _private.get(this) 304 | let { gotRequestCollection, enabled } = _scope 305 | if (!enabled) return 306 | let request = gotRequestCollection.findOne({ id: envelop.id }) 307 | 308 | if (!request) return 309 | 310 | request.error = true 311 | request.size.push(envelop.size) 312 | 313 | gotRequestCollection.update(request) 314 | 315 | if (++_scope.count === 1000) { 316 | this::_updateAggregationTable() 317 | } 318 | } 319 | 320 | gotReplySuccess (envelop) { 321 | let _scope = _private.get(this) 322 | let { sendRequestCollection, enabled } = _scope 323 | if (!enabled) return 324 | let request = sendRequestCollection.findOne({ id: envelop.id }) 325 | 326 | if (!request) return 327 | 328 | request.success = true 329 | request.duration = envelop.data.duration 330 | request.size.push(envelop.size) 331 | sendRequestCollection.update(request) 332 | 333 | if (++_scope.count === 1000) { 334 | this::_updateAggregationTable() 335 | } 336 | } 337 | 338 | gotReplyError (envelop) { 339 | let _scope = _private.get(this) 340 | let { sendRequestCollection, enabled } = _scope 341 | if (!enabled) return 342 | let request = sendRequestCollection.findOne({ id: envelop.id }) 343 | 344 | if (!request) return 345 | 346 | request.error = true 347 | request.duration = envelop.data.duration 348 | request.size.push(envelop.size) 349 | sendRequestCollection.update(request) 350 | } 351 | 352 | requestTimeout (envelop) { 353 | let _scope = _private.get(this) 354 | let { sendRequestCollection, enabled } = _scope 355 | if (!enabled) return 356 | let request = sendRequestCollection.findOne({ id: envelop.id }) 357 | 358 | if (!request) return 359 | 360 | request.timeout = true 361 | sendRequestCollection.update(request) 362 | 363 | if (++_scope.count === 1000) { 364 | this::_updateAggregationTable() 365 | } 366 | } 367 | 368 | sendTick (envelop) { 369 | let _scope = _private.get(this) 370 | let { sendTickCollection, enabled } = _scope 371 | if (!enabled) return 372 | let tickInstance = MetricUtils.createTick(envelop) 373 | sendTickCollection.insert(tickInstance) 374 | 375 | if (++_scope.count === 1000) { 376 | this::_updateAggregationTable() 377 | } 378 | } 379 | 380 | gotTick (envelop) { 381 | let _scope = _private.get(this) 382 | let { gotTickCollection, enabled } = _scope 383 | if (!enabled) return 384 | let tickInstance = MetricUtils.createTick(envelop) 385 | gotTickCollection.insert(tickInstance) 386 | 387 | if (++_scope.count === 1000) { 388 | this::_updateAggregationTable() 389 | } 390 | } 391 | 392 | flush () { 393 | let _scope = _private.get(this) 394 | let { sendRequestCollection, sendTickCollection, gotRequestCollection, gotTickCollection } = _scope 395 | sendRequestCollection.removeWhere(finishedPredicate) 396 | sendTickCollection.removeWhere(finishedPredicate) 397 | gotRequestCollection.removeWhere(truePredicate) 398 | gotTickCollection.removeWhere(truePredicate) 399 | _scope.count = 0 400 | } 401 | } 402 | -------------------------------------------------------------------------------- /src/node.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by avar and dave on 2/14/17. 3 | */ 4 | import winston from 'winston' 5 | import _ from 'underscore' 6 | import Promise from 'bluebird' 7 | import md5 from 'md5' 8 | import animal from 'animal-id' 9 | import { EventEmitter } from 'events' 10 | 11 | import { ZeronodeError, ErrorCodes } from './errors' 12 | import NodeUtils from './utils' 13 | import Server from './server' 14 | import Client from './client' 15 | import Metric from './metric' 16 | import { events } from './enum' 17 | import { Enum, Watchers } from './sockets' 18 | 19 | let MetricType = Enum.MetricType 20 | 21 | const _private = new WeakMap() 22 | 23 | let defaultLogger = winston.createLogger({ 24 | transports: [ 25 | new (winston.transports.Console)({ level: 'error' }) 26 | ] 27 | }) 28 | 29 | export default class Node extends EventEmitter { 30 | constructor ({ id, bind, options, config } = {}) { 31 | super() 32 | 33 | id = id || _generateNodeId() 34 | options = options || {} 35 | Object.defineProperty(options, '_id', { 36 | value: id, 37 | writable: false, 38 | configurable: true, 39 | enumerable: true 40 | }) 41 | config = config || {} 42 | config.logger = defaultLogger 43 | 44 | this.logger = config.logger || defaultLogger 45 | 46 | // ** default metric is disabled 47 | let metric = new Metric({ id }) 48 | 49 | let _scope = { 50 | id, 51 | bind, 52 | options, 53 | config, 54 | metric, 55 | nodeServer: null, 56 | nodeClients: new Map(), 57 | nodeClientsAddressIndex: new Map(), 58 | tickWatcherMap: new Map(), 59 | requestWatcherMap: new Map() 60 | } 61 | 62 | _private.set(this, _scope) 63 | this::_initNodeServer() 64 | } 65 | 66 | getId () { 67 | let { id } = _private.get(this) 68 | return id 69 | } 70 | 71 | getAddress () { 72 | let { nodeServer } = _private.get(this) 73 | return nodeServer ? nodeServer.getAddress() : null 74 | } 75 | 76 | getOptions () { 77 | let { options } = _private.get(this) 78 | return options 79 | } 80 | 81 | getServerInfo ({ address, id }) { 82 | let { nodeClients, nodeClientsAddressIndex } = _private.get(this) 83 | 84 | if (!id) { 85 | let addressHash = md5(address) 86 | 87 | if (!nodeClientsAddressIndex.has(addressHash)) return null 88 | id = nodeClientsAddressIndex.get(addressHash) 89 | } 90 | 91 | let client = nodeClients.get(id) 92 | 93 | if (!client) return null 94 | 95 | let serverActor = client.getServerActor() 96 | 97 | return serverActor ? serverActor.toJSON() : null 98 | } 99 | 100 | getClientInfo ({ id }) { 101 | let { nodeServer } = _private.get(this) 102 | 103 | let client = nodeServer.getClientById(id) 104 | 105 | return client ? client.toJSON() : null 106 | } 107 | 108 | getFilteredNodes ({ options, predicate, up = true, down = true } = {}) { 109 | let _scope = _private.get(this) 110 | let nodes = new Set() 111 | 112 | // ** if the predicate is provided we'll use it, if not then filtering will hapen based on options 113 | // ** options predicate is built via NodeUtils.optionsPredicateBuilder 114 | predicate = _.isFunction(predicate) ? predicate : NodeUtils.optionsPredicateBuilder(options) 115 | 116 | if (_scope.nodeServer && down) { 117 | _scope.nodeServer.getOnlineClients().forEach((clientNode) => { 118 | NodeUtils.checkNodeReducer(clientNode, predicate, nodes) 119 | }, this) 120 | } 121 | 122 | if (_scope.nodeClients.size && up) { 123 | _scope.nodeClients.forEach((client) => { 124 | let actorModel = client.getServerActor() 125 | if (actorModel && actorModel.isOnline()) { 126 | NodeUtils.checkNodeReducer(actorModel, predicate, nodes) 127 | } 128 | }, this) 129 | } 130 | 131 | return Array.from(nodes) 132 | } 133 | 134 | setAddress (bind) { 135 | let { nodeServer } = _private.get(this) 136 | nodeServer ? nodeServer.setAddress(bind) : this.logger.info('No server available') 137 | } 138 | 139 | // ** returns promise 140 | bind (address) { 141 | let { nodeServer } = _private.get(this) 142 | return nodeServer.bind(address) 143 | } 144 | 145 | // ** returns promise 146 | unbind () { 147 | let { nodeServer } = _private.get(this) 148 | if (!nodeServer) return Promise.resolve() 149 | 150 | return nodeServer.unbind() 151 | } 152 | 153 | // ** connect returns the id of the connected node 154 | async connect ({ address, timeout, reconnectionTimeout } = {}) { 155 | if (typeof address !== 'string' || address.length === 0) { 156 | throw new Error(`Wrong type for argument address ${address}`) 157 | } 158 | 159 | let _scope = _private.get(this) 160 | let { id, metric, nodeClientsAddressIndex, nodeClients, config } = _scope 161 | let clientConfig = config 162 | 163 | if (reconnectionTimeout) clientConfig = Object.assign({}, config, { RECONNECTION_TIMEOUT: reconnectionTimeout }) 164 | 165 | address = address || 'tcp://127.0.0.1:3000' 166 | 167 | let addressHash = md5(address) 168 | 169 | if (nodeClientsAddressIndex.has(addressHash)) { 170 | let client = nodeClients.get(nodeClientsAddressIndex.get(addressHash)) 171 | return client.getServerActor().toJSON() 172 | } 173 | 174 | let client = new Client({ id, options: _scope.options, config: clientConfig }) 175 | 176 | // ** attaching client handlers 177 | client.on('error', (err) => this.emit('error', err)) 178 | client.on(events.SERVER_FAILURE, (serverActor) => this.emit(events.SERVER_FAILURE, serverActor)) 179 | client.on(events.SERVER_STOP, (serverActor) => this.emit(events.SERVER_STOP, serverActor)) 180 | client.on(events.SERVER_RECONNECT, (serverActor) => { 181 | try { 182 | let addressHash = md5(serverActor.address) 183 | let oldId = nodeClientsAddressIndex.get(addressHash) 184 | nodeClients.delete(oldId) 185 | nodeClientsAddressIndex.set(addressHash, serverActor.id) 186 | nodeClients.set(serverActor.id, client) 187 | } catch (err) { 188 | this.logger.error('Error while handling server reconnect', err) 189 | } 190 | this.emit(events.SERVER_RECONNECT, serverActor) 191 | }) 192 | client.on(events.SERVER_RECONNECT_FAILURE, (serverActor) => { 193 | try { 194 | nodeClients.delete(serverActor.id) 195 | nodeClientsAddressIndex.delete(md5(serverActor.address)) 196 | } catch (err) { 197 | this.logger.error('Error while handling server reconnect failure', err) 198 | } 199 | this.emit(events.SERVER_RECONNECT_FAILURE, serverActor) 200 | }) 201 | client.on(events.OPTIONS_SYNC, ({ id, newOptions }) => this.emit(events.OPTIONS_SYNC, { id, newOptions })) 202 | 203 | // ** 204 | client.setMetric(metric.status) 205 | 206 | this::_addExistingListenersToClient(client) 207 | 208 | let { actorId } = await client.connect(address, timeout) 209 | 210 | this::_attachMetricsHandlers(client, metric) 211 | 212 | this.logger.info(`Node connected: ${this.getId()} -> ${actorId}`) 213 | 214 | nodeClientsAddressIndex.set(addressHash, actorId) 215 | nodeClients.set(actorId, client) 216 | 217 | this.emit(events.CONNECT_TO_SERVER, client.getServerActor().toJSON()) 218 | 219 | return client.getServerActor().toJSON() 220 | } 221 | 222 | // TODO::avar maybe disconnect from node ? 223 | async disconnect (address = 'tcp://127.0.0.1:3000') { 224 | if (typeof address !== 'string' || address.length === 0) { 225 | throw new Error(`Wrong type for argument address ${address}`) 226 | } 227 | 228 | let addressHash = md5(address) 229 | 230 | let _scope = _private.get(this) 231 | let { nodeClientsAddressIndex, nodeClients } = _scope 232 | 233 | if (!nodeClientsAddressIndex.has(addressHash)) return true 234 | 235 | let nodeId = nodeClientsAddressIndex.get(addressHash) 236 | let client = nodeClients.get(nodeId) 237 | 238 | client.removeAllListeners(events.SERVER_FAILURE) 239 | client.removeAllListeners(MetricType.SEND_TICK) 240 | client.removeAllListeners(MetricType.GOT_TICK) 241 | client.removeAllListeners(MetricType.SEND_REQUEST) 242 | client.removeAllListeners(MetricType.GOT_REQUEST) 243 | client.removeAllListeners(MetricType.SEND_REPLY_SUCCESS) 244 | client.removeAllListeners(MetricType.SEND_REPLY_ERROR) 245 | client.removeAllListeners(MetricType.GOT_REPLY_SUCCESS) 246 | client.removeAllListeners(MetricType.GOT_REPLY_ERROR) 247 | client.removeAllListeners(MetricType.REQUEST_TIMEOUT) 248 | client.removeAllListeners(MetricType.OPTIONS_SYNC) 249 | 250 | await client.disconnect() 251 | this::_removeClientAllListeners(client) 252 | nodeClients.delete(nodeId) 253 | nodeClientsAddressIndex.delete(addressHash) 254 | return true 255 | } 256 | 257 | async stop () { 258 | let { nodeServer, nodeClients } = _private.get(this) 259 | let stopPromise = [] 260 | 261 | this.disableMetrics() 262 | 263 | if (nodeServer.isOnline()) { 264 | stopPromise.push(nodeServer.close()) 265 | } 266 | 267 | nodeClients.forEach((client) => { 268 | stopPromise.push(client.close()) 269 | }, this) 270 | 271 | await Promise.all(stopPromise) 272 | } 273 | 274 | onRequest (requestEvent, fn) { 275 | let _scope = _private.get(this) 276 | let { requestWatcherMap, nodeClients, nodeServer } = _scope 277 | 278 | let requestWatcher = requestWatcherMap.get(requestEvent) 279 | if (!requestWatcher) { 280 | requestWatcher = new Watchers(requestEvent) 281 | requestWatcherMap.set(requestEvent, requestWatcher) 282 | } 283 | 284 | requestWatcher.addFn(fn) 285 | 286 | nodeServer.onRequest(requestEvent, fn) 287 | 288 | nodeClients.forEach((client) => { 289 | client.onRequest(requestEvent, fn) 290 | }, this) 291 | } 292 | 293 | offRequest (requestEvent, fn) { 294 | let _scope = _private.get(this) 295 | 296 | _scope.nodeServer.offRequest(requestEvent, fn) 297 | _scope.nodeClients.forEach((client) => { 298 | client.offRequest(requestEvent, fn) 299 | }) 300 | 301 | let requestWatcher = _scope.requestWatcherMap.get(requestEvent) 302 | if (requestWatcher) { 303 | requestWatcher.removeFn(fn) 304 | } 305 | } 306 | 307 | onTick (event, fn) { 308 | let _scope = _private.get(this) 309 | let { tickWatcherMap, nodeClients, nodeServer } = _scope 310 | 311 | let tickWatcher = tickWatcherMap.get(event) 312 | if (!tickWatcher) { 313 | tickWatcher = new Watchers(event) 314 | tickWatcherMap.set(event, tickWatcher) 315 | } 316 | 317 | tickWatcher.addFn(fn) 318 | 319 | // ** _scope.nodeServer is constructed in Node constructor 320 | nodeServer.onTick(event, fn) 321 | 322 | nodeClients.forEach((client) => { 323 | client.onTick(event, fn) 324 | }) 325 | } 326 | 327 | offTick (event, fn) { 328 | let _scope = _private.get(this) 329 | _scope.nodeServer.offTick(event) 330 | _scope.nodeClients.forEach((client) => { 331 | client.offTick(event, fn) 332 | }, this) 333 | 334 | let tickWatcher = _scope.tickWatcherMap.get(event) 335 | if (tickWatcher) { 336 | tickWatcher.removeFn(fn) 337 | } 338 | } 339 | 340 | async request ({ to, event, data, timeout } = {}) { 341 | let _scope = _private.get(this) 342 | 343 | let { nodeServer, nodeClients } = _scope 344 | 345 | let clientActor = this::_getClientByNode(to) 346 | if (clientActor) { 347 | return nodeServer.request({ to: clientActor.getId(), event, data, timeout }) 348 | } 349 | 350 | if (nodeClients.has(to)) { 351 | // ** to is the serverId of node so we request 352 | return nodeClients.get(to).request({ event, data, timeout }) 353 | } 354 | 355 | throw new ZeronodeError({ message: `Node with id '${to}' is not found.`, code: ErrorCodes.NODE_NOT_FOUND }) 356 | } 357 | 358 | tick ({ to, event, data } = {}) { 359 | let _scope = _private.get(this) 360 | let { nodeServer, nodeClients } = _scope 361 | let clientActor = this::_getClientByNode(to) 362 | if (clientActor) { 363 | return nodeServer.tick({ to: clientActor.getId(), event, data }) 364 | } 365 | if (nodeClients.has(to)) { 366 | return nodeClients.get(to).tick({ event, data }) 367 | } 368 | throw new ZeronodeError({ message: `Node with id '${to}' is not found.`, code: ErrorCodes.NODE_NOT_FOUND }) 369 | } 370 | 371 | async requestAny ({ event, data, timeout, filter, down = true, up = true } = {}) { 372 | let nodesFilter = { down, up } 373 | if (_.isFunction(filter)) { 374 | nodesFilter.predicate = filter 375 | } else { 376 | nodesFilter.options = filter || {} 377 | } 378 | 379 | let filteredNodes = this.getFilteredNodes(nodesFilter) 380 | 381 | if (!filteredNodes.length) { 382 | throw new ZeronodeError({ message: `Node with filter is not found.`, code: ErrorCodes.NODE_NOT_FOUND }) 383 | } 384 | 385 | // ** find the node id where the request will be sent 386 | let to = this::_getWinnerNode(filteredNodes, event) 387 | return this.request({ to, event, data, timeout }) 388 | } 389 | 390 | async requestDownAny ({ event, data, timeout, filter } = {}) { 391 | let result = await this.requestAny({ event, data, timeout, filter, down: true, up: false }) 392 | return result 393 | } 394 | 395 | async requestUpAny ({ event, data, timeout, filter } = {}) { 396 | let result = await this.requestAny({ event, data, timeout, filter, down: false, up: true }) 397 | return result 398 | } 399 | 400 | tickAny ({ event, data, filter, down = true, up = true } = {}) { 401 | let nodesFilter = { down, up } 402 | if (_.isFunction(filter)) { 403 | nodesFilter.predicate = filter 404 | } else { 405 | nodesFilter.options = filter || {} 406 | } 407 | 408 | let filteredNodes = this.getFilteredNodes(nodesFilter) 409 | 410 | if (!filteredNodes.length) { 411 | throw new ZeronodeError({ message: `Node with filter is not found.`, code: ErrorCodes.NODE_NOT_FOUND }) 412 | } 413 | let nodeId = this::_getWinnerNode(filteredNodes, event) 414 | return this.tick({ to: nodeId, event, data }) 415 | } 416 | 417 | tickDownAny ({ event, data, filter } = {}) { 418 | return this.tickAny({ event, data, filter, down: true, up: false }) 419 | } 420 | 421 | tickUpAny ({ event, data, filter } = {}) { 422 | return this.tickAny({ event, data, filter, down: false, up: true }) 423 | } 424 | 425 | tickAll ({ event, data, filter, down = true, up = true } = {}) { 426 | let nodesFilter = { down, up } 427 | if (_.isFunction(filter)) { 428 | nodesFilter.predicate = filter 429 | } else { 430 | nodesFilter.options = filter || {} 431 | } 432 | 433 | let filteredNodes = this.getFilteredNodes(nodesFilter) 434 | let tickPromises = [] 435 | 436 | filteredNodes.forEach((nodeId) => { 437 | tickPromises.push(this.tick({ to: nodeId, event, data })) 438 | }, this) 439 | 440 | return Promise.all(tickPromises) 441 | } 442 | 443 | tickDownAll ({ event, data, filter } = {}) { 444 | return this.tickAll({ event, data, filter, down: true, up: false }) 445 | } 446 | 447 | tickUpAll ({ event, data, filter } = {}) { 448 | return this.tickAll({ event, data, filter, down: false, up: true }) 449 | } 450 | 451 | enableMetrics (flushInterval) { 452 | let _scope = _private.get(this) 453 | 454 | let { metric, nodeClients, nodeServer } = _scope 455 | metric.enable(flushInterval) 456 | 457 | nodeClients.forEach((client) => { 458 | client.setMetric(true) 459 | }, this) 460 | 461 | nodeServer.setMetric(true) 462 | } 463 | 464 | get metric () { 465 | let { metric } = _private.get(this) 466 | return metric 467 | } 468 | 469 | disableMetrics () { 470 | let { metric, nodeClients, nodeServer } = _private.get(this) 471 | metric.disable() 472 | 473 | nodeClients.forEach((client) => { 474 | client.setMetric(false) 475 | }, this) 476 | nodeServer.setMetric(false) 477 | } 478 | 479 | async setOptions (options = {}) { 480 | let _scope = _private.get(this) 481 | _scope.options = options 482 | 483 | Object.defineProperty(options, '_id', { 484 | value: _scope.id, 485 | writable: false, 486 | configurable: true, 487 | enumerable: true 488 | }) 489 | 490 | let { nodeServer, nodeClients } = _scope 491 | nodeServer.setOptions(options) 492 | nodeClients.forEach((client) => { 493 | client.setOptions(options) 494 | }, this) 495 | } 496 | } 497 | 498 | // ** PRIVATE FUNCTIONS 499 | 500 | function _initNodeServer () { 501 | let _scope = _private.get(this) 502 | let { id, bind, options, metric, config } = _scope 503 | 504 | let nodeServer = new Server({ id, bind, options, config }) 505 | // ** handlers for nodeServer 506 | nodeServer.on('error', (err) => this.emit('error', err)) 507 | nodeServer.on(events.CLIENT_FAILURE, (clientActor) => this.emit(events.CLIENT_FAILURE, clientActor)) 508 | nodeServer.on(events.CLIENT_CONNECTED, (clientActor) => this.emit(events.CLIENT_CONNECTED, clientActor)) 509 | nodeServer.on(events.CLIENT_STOP, (clientActor) => this.emit(events.CLIENT_STOP, clientActor)) 510 | nodeServer.on(events.OPTIONS_SYNC, ({ id, newOptions }) => this.emit(events.OPTIONS_SYNC, { id, newOptions })) 511 | 512 | // ** enabling metrics 513 | nodeServer.setMetric(metric.status) 514 | this::_attachMetricsHandlers(nodeServer, metric) 515 | 516 | _scope.nodeServer = nodeServer 517 | } 518 | 519 | function _getClientByNode (nodeId) { 520 | let _scope = _private.get(this) 521 | let actors = _scope.nodeServer.getOnlineClients().filter((actor) => { 522 | let node = actor.getId() 523 | return node === nodeId 524 | }) 525 | 526 | if (!actors.length) { 527 | return null 528 | } 529 | 530 | if (actors.length > 1) { 531 | return this.logger.warn(`We should have just 1 client from 1 node`) 532 | } 533 | 534 | return actors[0] 535 | } 536 | 537 | function _generateNodeId () { 538 | return animal.getId() 539 | } 540 | 541 | // TODO::avar optimize this 542 | function _getWinnerNode (nodeIds, tag) { 543 | let len = nodeIds.length 544 | let idx = Math.floor(Math.random() * len) 545 | return nodeIds[idx] 546 | } 547 | 548 | function _addExistingListenersToClient (client) { 549 | let _scope = _private.get(this) 550 | 551 | // ** adding previously added onTick-s for this client to 552 | _scope.tickWatcherMap.forEach((tickWatcher, event) => { 553 | // ** TODO what about order of functions ? 554 | tickWatcher.getFnMap().forEach((index, fn) => { 555 | client.onTick(event, this::fn) 556 | }, this) 557 | }, this) 558 | 559 | // ** adding previously added onRequests-s for this client to 560 | _scope.requestWatcherMap.forEach((requestWatcher, requestEvent) => { 561 | // ** TODO what about order of functions ? 562 | requestWatcher.getFnMap().forEach((index, fn) => { 563 | client.onRequest(requestEvent, this::fn) 564 | }, this) 565 | }, this) 566 | } 567 | 568 | function _removeClientAllListeners (client) { 569 | let _scope = _private.get(this) 570 | 571 | // ** removing all handlers 572 | _scope.tickWatcherMap.forEach((tickWatcher, event) => { 573 | client.offTick(event) 574 | }, this) 575 | 576 | // ** removing all handlers 577 | _scope.requestWatcherMap.forEach((requestWatcher, requestEvent) => { 578 | client.offRequest(requestEvent) 579 | }, this) 580 | } 581 | 582 | function _attachMetricsHandlers (socket, metric) { 583 | socket.on(MetricType.SEND_TICK, (envelop) => { 584 | this.emit(MetricType.SEND_TICK, envelop) 585 | metric.sendTick(envelop) 586 | }) 587 | 588 | socket.on(MetricType.SEND_REQUEST, (envelop) => { 589 | this.emit(MetricType.SEND_REQUEST, envelop) 590 | metric.sendRequest(envelop) 591 | }) 592 | 593 | socket.on(MetricType.SEND_REPLY_SUCCESS, (envelop) => { 594 | this.emit(MetricType.SEND_REPLY_SUCCESS, envelop) 595 | metric.sendReplySuccess(envelop) 596 | }) 597 | 598 | socket.on(MetricType.SEND_REPLY_ERROR, (envelop) => { 599 | this.emit(MetricType.SEND_REPLY_ERROR, envelop) 600 | metric.sendReplyError(envelop) 601 | }) 602 | 603 | socket.on(MetricType.REQUEST_TIMEOUT, (envelop) => { 604 | this.emit(MetricType.REQUEST_TIMEOUT, envelop) 605 | metric.requestTimeout(envelop) 606 | }) 607 | 608 | socket.on(MetricType.GOT_TICK, (envelop) => { 609 | this.emit(MetricType.GOT_TICK, envelop) 610 | metric.gotTick(envelop) 611 | }) 612 | 613 | socket.on(MetricType.GOT_REQUEST, (envelop) => { 614 | this.emit(MetricType.GOT_REQUEST, envelop) 615 | metric.gotRequest(envelop) 616 | }) 617 | 618 | socket.on(MetricType.GOT_REPLY_SUCCESS, (envelop) => { 619 | this.emit(MetricType.GOT_REPLY_SUCCESS, envelop) 620 | metric.gotReplySuccess(envelop) 621 | }) 622 | 623 | socket.on(MetricType.GOT_REPLY_ERROR, (envelop) => { 624 | this.emit(MetricType.GOT_REPLY_ERROR, envelop) 625 | metric.gotReplyError(envelop) 626 | }) 627 | } 628 | -------------------------------------------------------------------------------- /src/server.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore' 2 | 3 | import { events } from './enum' 4 | import Globals from './globals' 5 | import ActorModel from './actor' 6 | import { ZeronodeError, ErrorCodes } from './errors' 7 | 8 | import { Router as RouterSocket } from './sockets' 9 | 10 | let _private = new WeakMap() 11 | 12 | export default class Server extends RouterSocket { 13 | constructor ({ id, bind, config, options } = {}) { 14 | options = options || {} 15 | config = config || {} 16 | 17 | super({ id, options, config }) 18 | 19 | let _scope = { 20 | clientModels: new Map(), 21 | clientCheckInterval: null 22 | } 23 | 24 | _private.set(this, _scope) 25 | 26 | this.setAddress(bind) 27 | 28 | // ** ATTACHING client connected 29 | this.onRequest(events.CLIENT_CONNECTED, this::_clientConnectedRequest, true) 30 | 31 | // ** ATTACHING client stop 32 | this.onRequest(events.CLIENT_STOP, this::_clientStopRequest, true) 33 | 34 | // ** ATTACHING client ping 35 | this.onTick(events.CLIENT_PING, this::_clientPingTick, true) 36 | 37 | // ** ATTACHING CLIENT OPTIONS SYNCING 38 | this.onTick(events.OPTIONS_SYNC, this::_clientOptionsSync, true) 39 | } 40 | 41 | getClientById (clientId) { 42 | let { clientModels } = _private.get(this) 43 | return clientModels.has(clientId) ? clientModels.get(clientId) : null 44 | } 45 | 46 | getOnlineClients () { 47 | let { clientModels } = _private.get(this) 48 | let onlineClients = [] 49 | clientModels.forEach((actor) => { 50 | if (actor.isOnline()) { 51 | onlineClients.push(actor) 52 | } 53 | }, this) 54 | 55 | return onlineClients 56 | } 57 | 58 | setOptions (options, notify = true) { 59 | super.setOptions(options) 60 | if (notify && this.isOnline()) { 61 | _.each(this.getOnlineClients(), (client) => { 62 | this.tick({ event: events.OPTIONS_SYNC, data: { actorId: this.getId(), options }, to: client.id, mainEvent: true }) 63 | }) 64 | } 65 | } 66 | 67 | bind (bindAddress) { 68 | if (_.isString(bindAddress)) { 69 | this.setAddress(bindAddress) 70 | } 71 | return super.bind(this.getAddress()) 72 | } 73 | 74 | unbind () { 75 | try { 76 | let _scope = _private.get(this) 77 | 78 | if (this.isOnline()) { 79 | _.each(this.getOnlineClients(), (client) => { 80 | this.tick({ to: client.getId(), event: events.SERVER_STOP, mainEvent: true }) 81 | }) 82 | } 83 | 84 | // ** clear the heartbeat checking interval 85 | if (_scope.clientCheckInterval) { 86 | clearInterval(_scope.clientCheckInterval) 87 | } 88 | _scope.clientCheckInterval = null 89 | 90 | return super.unbind() 91 | } catch (err) { 92 | let serverUnbindError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.SERVER_UNBIND, error: err }) 93 | return Promise.reject(serverUnbindError) 94 | } 95 | } 96 | } 97 | 98 | // ** Request handlers 99 | function _clientPingTick ({ actor, stamp }) { 100 | let { clientModels } = _private.get(this) 101 | // ** PING DATA FROM CLIENT, actor is client id 102 | 103 | let actorModel = clientModels.get(actor) 104 | 105 | if (actorModel) { 106 | actorModel.ping(stamp) 107 | } 108 | } 109 | 110 | function _clientStopRequest (request) { 111 | let { clientModels } = _private.get(this) 112 | let { actorId, options } = request.body 113 | 114 | // ** just replying acknowledgment 115 | request.reply({ stamp: Date.now() }) 116 | 117 | let actorModel = clientModels.get(actorId) 118 | if(!actorModel) return 119 | 120 | actorModel.markStopped() 121 | actorModel.mergeOptions(options) 122 | 123 | this.emit(events.CLIENT_STOP, actorModel.toJSON()) 124 | } 125 | 126 | function _clientConnectedRequest (request) { 127 | let _scope = _private.get(this) 128 | let { clientModels, clientCheckInterval } = _scope 129 | 130 | let { actorId, options } = request.body 131 | 132 | let actorModel = new ActorModel({ id: actorId, options: options, online: true }) 133 | 134 | clientModels.set(actorId, actorModel) 135 | 136 | if (!clientCheckInterval) { 137 | let config = this.getConfig() 138 | let clientHeartbeatInterval = config.CLIENT_MUST_HEARTBEAT_INTERVAL || Globals.CLIENT_MUST_HEARTBEAT_INTERVAL 139 | _scope.clientCheckInterval = setInterval(this::_checkClientHeartBeat, clientHeartbeatInterval) 140 | } 141 | 142 | let replyData = { actorId: this.getId(), options: this.getOptions() } 143 | // ** replyData {actorId, options} 144 | request.reply(replyData) 145 | 146 | this.emit(events.CLIENT_CONNECTED, actorModel.toJSON()) 147 | } 148 | 149 | // ** check clients heartbeat 150 | function _checkClientHeartBeat () { 151 | _.each(this.getOnlineClients(), (actor) => { 152 | if (!actor.isGhost()) { 153 | actor.markGhost() 154 | } else { 155 | actor.markFailed() 156 | this.emit(events.CLIENT_FAILURE, actor.toJSON()) 157 | } 158 | }) 159 | } 160 | 161 | function _clientOptionsSync ({ actorId, options }) { 162 | try { 163 | let { clientModels } = _private.get(this) 164 | let actorModel = clientModels.get(actorId) 165 | // TODO::remove after some time 166 | if (!actorModel) { 167 | throw new Error(`Client actor '${actorId}' is not available on server '${this.getId()}'`) 168 | } 169 | actorModel.setOptions(options) 170 | this.emit(events.OPTIONS_SYNC, { id: actorModel.getId(), newOptions: options }) 171 | } catch (err) { 172 | let clientOptionsSyncHandlerError = new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.CLIENT_OPTIONS_SYNC_HANDLER, error: err }) 173 | clientOptionsSyncHandlerError.description = `Error while handling client options sync on server ${this.getId()}` 174 | this.emit('error', clientOptionsSyncHandlerError) 175 | } 176 | } 177 | -------------------------------------------------------------------------------- /src/sockets/dealer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by artak on 3/2/17. 3 | */ 4 | 5 | import Promise from 'bluebird' 6 | import zmq from 'zeromq' 7 | 8 | import { ZeronodeError, ErrorCodes } from '../errors' 9 | import { Socket, SocketEvent } from './socket' 10 | import Envelop from './envelope' 11 | import { EnvelopType, DealerStateType, Timeouts } from './enum' 12 | 13 | let _private = new WeakMap() 14 | 15 | export default class DealerSocket extends Socket { 16 | constructor ({ id, options, config } = {}) { 17 | options = options || {} 18 | config = config || {} 19 | 20 | let socket = zmq.socket('dealer') 21 | 22 | super({ id, socket, options, config }) 23 | 24 | let _scope = { 25 | socket, 26 | state: DealerStateType.DISCONNECTED, 27 | connectionPromise: null, 28 | routerAddress: null 29 | } 30 | 31 | _private.set(this, _scope) 32 | } 33 | 34 | getAddress () { 35 | let { routerAddress } = _private.get(this) 36 | return routerAddress 37 | } 38 | 39 | setAddress (routerAddress) { 40 | let _scope = _private.get(this) 41 | if (typeof routerAddress === 'string' && routerAddress.length) { 42 | _scope.routerAddress = routerAddress 43 | } 44 | } 45 | 46 | setOnline () { 47 | let _scope = _private.get(this) 48 | super.setOnline() 49 | _scope.state = DealerStateType.CONNECTED 50 | } 51 | 52 | getState () { 53 | let { state } = _private.get(this) 54 | return state 55 | } 56 | 57 | connect (routerAddress, timeout) { 58 | if (this.isOnline() && routerAddress === this.getAddress()) { 59 | return Promise.resolve(true) 60 | } 61 | 62 | let _scope = _private.get(this) 63 | let connectionPromise = _scope.connectionPromise 64 | timeout = timeout || this.getConfig().CONNECTION_TIMEOUT || Timeouts.CONNECTION_TIMEOUT 65 | 66 | if (connectionPromise && routerAddress !== this.getAddress()) { 67 | // ** if trying to connect to other address you need to disconnect first 68 | let alreadyConnectedError = new Error(`Already connected to '${this.getAddress()}', disconnect before changing connection address to '${routerAddress}'`) 69 | return Promise.reject(new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.ALREADY_CONNECTED, error: alreadyConnectedError })) 70 | } 71 | 72 | // ** if connection is still pending then returning it 73 | if (connectionPromise && connectionPromise.isPending() && routerAddress === this.getAddress()) return connectionPromise 74 | 75 | // ** if connect is called for the first time then creating the connection promise 76 | _scope.connectionPromise = new Promise((resolve, reject) => { 77 | let { socket } = _scope 78 | let { RECONNECTION_TIMEOUT } = this.getConfig() 79 | RECONNECTION_TIMEOUT = RECONNECTION_TIMEOUT || Timeouts.RECONNECTION_TIMEOUT 80 | 81 | let rejectionTimeout = null 82 | 83 | if (routerAddress) { 84 | this.setAddress(routerAddress) 85 | } 86 | 87 | const onConnectionHandler = () => { 88 | if (rejectionTimeout) { 89 | clearTimeout(rejectionTimeout) 90 | } 91 | 92 | this.once(SocketEvent.DISCONNECT, onDisconnectionHandler) 93 | 94 | this.setOnline() 95 | resolve() 96 | } 97 | 98 | const onReConnectionHandler = (fd, endpoint) => { 99 | if (_scope.reconnectionTimeoutInstance) { 100 | clearTimeout(_scope.reconnectionTimeoutInstance) 101 | _scope.reconnectionTimeoutInstance = null 102 | } 103 | 104 | this.once(SocketEvent.DISCONNECT, onDisconnectionHandler) 105 | this.setOnline() 106 | this.emit(SocketEvent.RECONNECT, { fd, endpoint }) 107 | } 108 | 109 | const onDisconnectionHandler = () => { 110 | this.setOffline() 111 | _scope.state = DealerStateType.RECONNECTING 112 | this.once(SocketEvent.CONNECT, onReConnectionHandler) 113 | if (RECONNECTION_TIMEOUT !== Timeouts.INFINITY) { 114 | _scope.reconnectionTimeoutInstance = setTimeout(() => { 115 | // ** removing reconnection listener 116 | this.removeListener(SocketEvent.CONNECT, onReConnectionHandler) 117 | // ** disconnecting socket 118 | this.emit(SocketEvent.RECONNECT_FAILURE) 119 | this.disconnect() 120 | }, RECONNECTION_TIMEOUT) 121 | } 122 | } 123 | 124 | if (timeout !== Timeouts.INFINITY) { 125 | rejectionTimeout = setTimeout(() => { 126 | this.removeListener(SocketEvent.CONNECT, onConnectionHandler) 127 | // ** reject the connection promise and then disconnect 128 | let connectionTimeoutError = new Error(`Timeout to connect to ${this.getAddress()} `) 129 | reject(new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.CONNECTION_TIMEOUT, error: connectionTimeoutError })) 130 | this.disconnect() 131 | }, timeout) 132 | } 133 | 134 | this.once(SocketEvent.CONNECT, onConnectionHandler) 135 | 136 | this.attachSocketMonitor() 137 | 138 | socket.connect(this.getAddress()) 139 | }) 140 | 141 | return _scope.connectionPromise 142 | } 143 | 144 | // ** not actually disconnected 145 | disconnect () { 146 | //* closing and removing all listeners on socket 147 | super.close() 148 | 149 | let _scope = _private.get(this) 150 | let { socket, routerAddress, connectionPromise, reconnectionTimeoutInstance } = _scope 151 | 152 | //* if connection promise is pending then rejecting it 153 | if (connectionPromise && connectionPromise.isPending()) { 154 | connectionPromise.reject('Disconnecting') 155 | } 156 | 157 | if (reconnectionTimeoutInstance) { 158 | clearTimeout(reconnectionTimeoutInstance) 159 | _scope.reconnectionTimeoutInstance = null 160 | } 161 | 162 | _scope.connectionPromise = null 163 | 164 | if (this.getState() !== DealerStateType.DISCONNECTED) { 165 | socket.disconnect(routerAddress) 166 | _scope.state = DealerStateType.DISCONNECTED 167 | } 168 | 169 | this.setOffline() 170 | } 171 | 172 | // ** Polymorphic functions 173 | request ({ to, event, data, timeout, mainEvent = false } = {}) { 174 | let envelop = new Envelop({ type: EnvelopType.REQUEST, tag: event, data, owner: this.getId(), recipient: to, mainEvent }) 175 | return super.request(envelop, timeout) 176 | } 177 | 178 | tick ({ to, event, data, mainEvent = false } = {}) { 179 | let envelop = new Envelop({ type: EnvelopType.TICK, tag: event, data, owner: this.getId(), recipient: to, mainEvent }) 180 | return super.tick(envelop) 181 | } 182 | 183 | async close () { 184 | await this.disconnect() 185 | let { socket } = _private.get(this) 186 | 187 | socket.close() 188 | } 189 | 190 | getSocketMsg (envelop) { 191 | return envelop.getBuffer() 192 | } 193 | } 194 | -------------------------------------------------------------------------------- /src/sockets/enum.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by artak on 2/15/17. 3 | */ 4 | 5 | let EnvelopType = { 6 | TICK: 1, 7 | REQUEST: 2, 8 | RESPONSE: 3, 9 | ERROR: 4 10 | } 11 | 12 | let MetricType = { 13 | SEND_TICK: 'sendTick', 14 | SEND_REQUEST: 'sendRequest', 15 | SEND_REPLY_SUCCESS: 'sendReplySuccess', 16 | SEND_REPLY_ERROR: 'sendReplyError', 17 | GOT_TICK: 'gotTick', 18 | GOT_REQUEST: 'gotRequest', 19 | GOT_REPLY_SUCCESS: 'gotReplySuccess', 20 | GOT_REPLY_ERROR: 'gotReplyError', 21 | REQUEST_TIMEOUT: 'requestTimeout' 22 | } 23 | 24 | let Timeouts = { 25 | MONITOR_TIMEOUT: 10, 26 | // ** when monitor fials restart it after milliseconds 27 | MONITOR_RESTART_TIMEOUT: 1000, 28 | REQUEST_TIMEOUT: 10000, 29 | CONNECTION_TIMEOUT: -1, 30 | RECONNECTION_TIMEOUT: -1, 31 | INFINITY: -1 32 | } 33 | 34 | let DealerStateType = { 35 | CONNECTED: 'connected', 36 | DISCONNECTED: 'disconnected', 37 | RECONNECTING: 'reconnecting' 38 | } 39 | 40 | export { EnvelopType } 41 | export { MetricType } 42 | export { Timeouts } 43 | export { DealerStateType } 44 | 45 | export default { 46 | EnvelopType, 47 | MetricType, 48 | Timeouts, 49 | DealerStateType 50 | } 51 | -------------------------------------------------------------------------------- /src/sockets/envelope.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore' 2 | import crypto from 'crypto' 3 | import BufferAlloc from 'buffer-alloc' 4 | import BufferFrom from 'buffer-from' 5 | 6 | class Parse { 7 | // serialize 8 | static dataToBuffer (data) { 9 | try { 10 | return BufferFrom(JSON.stringify({ data })) 11 | } catch (err) { 12 | console.error(err) 13 | } 14 | } 15 | 16 | // deserialize 17 | static bufferToData (data) { 18 | try { 19 | let ob = JSON.parse(data.toString()) 20 | return ob.data 21 | } catch (err) { 22 | console.error(err) 23 | } 24 | } 25 | } 26 | 27 | const lengthSize = 1 28 | 29 | export default class Envelop { 30 | constructor ({ type, id = '', tag = '', data, owner = '', recipient = '', mainEvent }) { 31 | if (type) { 32 | this.setType(type) 33 | } 34 | 35 | this.id = id || crypto.randomBytes(20).toString('hex') 36 | this.tag = tag 37 | this.mainEvent = mainEvent 38 | 39 | if (data) { 40 | this.data = data 41 | } 42 | 43 | this.owner = owner 44 | this.recipient = recipient 45 | } 46 | 47 | toJSON () { 48 | return { 49 | type: this.type, 50 | id: this.id, 51 | tag: this.tag, 52 | data: this.data, 53 | owner: this.owner, 54 | recipient: this.recipient, 55 | mainEvent: this.mainEvent 56 | } 57 | } 58 | 59 | /** 60 | * 61 | * @param buffer 62 | * @description { 63 | * mainEvent: 1, 64 | * type: 1, 65 | * idLength: 4, 66 | * id: idLength, 67 | * ownerLength: 4, 68 | * owner: ownerLength, 69 | * recipientLength: 4, 70 | * recipient: recipientLength, 71 | * tagLength: 4, 72 | * tag: tagLength 73 | * @return {{mainEvent: boolean, type, id: string, owner: string, recipient: string, tag: string}} 74 | */ 75 | static readMetaFromBuffer (buffer) { 76 | let mainEvent = !!buffer.readInt8(0) 77 | 78 | let type = buffer.readInt8(1) 79 | 80 | let idStart = 2 + lengthSize 81 | let idLength = buffer.readInt8(idStart - lengthSize) 82 | let id = buffer.slice(idStart, idStart + idLength).toString('hex') 83 | 84 | let ownerStart = lengthSize + idStart + idLength 85 | let ownerLength = buffer.readInt8(ownerStart - lengthSize) 86 | let owner = buffer.slice(ownerStart, ownerStart + ownerLength).toString('utf8').replace(/\0/g, '') 87 | 88 | let recipientStart = lengthSize + ownerStart + ownerLength 89 | let recipientLength = buffer.readInt8(recipientStart - lengthSize) 90 | let recipient = buffer.slice(recipientStart, recipientStart + recipientLength).toString('utf8').replace(/\0/g, '') 91 | 92 | let tagStart = lengthSize + recipientStart + recipientLength 93 | let tagLength = buffer.readInt8(tagStart - lengthSize) 94 | let tag = buffer.slice(tagStart, tagStart + tagLength).toString('utf8').replace(/\0/g, '') 95 | 96 | return { mainEvent, type, id, owner, recipient, tag } 97 | } 98 | 99 | static readDataFromBuffer (buffer) { 100 | let dataBuffer = Envelop.getDataBuffer(buffer) 101 | return dataBuffer ? Parse.bufferToData(dataBuffer) : null 102 | } 103 | 104 | static getDataBuffer (buffer) { 105 | let metaLength = Envelop.getMetaLength(buffer) 106 | 107 | if (buffer.length > metaLength) { 108 | return buffer.slice(metaLength) 109 | } 110 | 111 | return null 112 | } 113 | 114 | static fromBuffer (buffer) { 115 | let { id, type, owner, recipient, tag, mainEvent } = Envelop.readMetaFromBuffer(buffer) 116 | let envelop = new Envelop({ type, id, tag, owner, recipient, mainEvent }) 117 | 118 | let envelopData = Envelop.readDataFromBuffer(buffer) 119 | if (envelopData) { 120 | envelop.setData(envelopData) 121 | } 122 | 123 | return envelop 124 | } 125 | 126 | static stringToBuffer (str, encryption) { 127 | let strLength = Buffer.byteLength(str, encryption) 128 | let lengthBuffer = BufferAlloc(lengthSize) 129 | lengthBuffer.writeInt8(strLength) 130 | let strBuffer = BufferAlloc(strLength) 131 | strBuffer.write(str, 0, strLength, encryption) 132 | return Buffer.concat([lengthBuffer, strBuffer]) 133 | } 134 | 135 | static getMetaLength (buffer) { 136 | let length = 2 137 | 138 | _.each(_.range(4), () => { 139 | length += lengthSize + buffer.readInt8(length) 140 | }) 141 | 142 | return length 143 | } 144 | 145 | getBuffer () { 146 | let bufferArray = [] 147 | 148 | let mainEventBuffer = BufferAlloc(1) 149 | mainEventBuffer.writeInt8(+this.mainEvent) 150 | bufferArray.push(mainEventBuffer) 151 | 152 | let typeBuffer = BufferAlloc(1) 153 | typeBuffer.writeInt8(this.type) 154 | bufferArray.push(typeBuffer) 155 | 156 | let idBuffer = Envelop.stringToBuffer(this.id.toString(), 'hex') 157 | bufferArray.push(idBuffer) 158 | 159 | let ownerBuffer = Envelop.stringToBuffer(this.owner.toString(), 'utf-8') 160 | bufferArray.push(ownerBuffer) 161 | 162 | let recipientBuffer = Envelop.stringToBuffer(this.recipient.toString(), 'utf-8') 163 | bufferArray.push(recipientBuffer) 164 | 165 | let tagBuffer = Envelop.stringToBuffer(this.tag.toString(), 'utf-8') 166 | bufferArray.push(tagBuffer) 167 | 168 | if (this.data) { 169 | bufferArray.push(Parse.dataToBuffer(this.data)) 170 | } 171 | 172 | return Buffer.concat(bufferArray) 173 | } 174 | 175 | getId () { 176 | return this.id 177 | } 178 | 179 | getTag () { 180 | return this.tag 181 | } 182 | 183 | getOwner () { 184 | return this.owner 185 | } 186 | 187 | setOwner (owner) { 188 | this.owner = owner 189 | } 190 | 191 | getRecipient () { 192 | return this.recipient 193 | } 194 | 195 | setRecipient (recipient) { 196 | this.recipient = recipient 197 | } 198 | 199 | // ** type of envelop 200 | 201 | getType () { 202 | return this.type 203 | } 204 | 205 | setType (type) { 206 | this.type = type 207 | } 208 | 209 | // ** data of envelop 210 | 211 | getData (data) { 212 | return this.data 213 | } 214 | 215 | setData (data) { 216 | this.data = data 217 | } 218 | 219 | isMain () { 220 | return !!this.mainEvent 221 | } 222 | } 223 | -------------------------------------------------------------------------------- /src/sockets/events.js: -------------------------------------------------------------------------------- 1 | const SocketEvent = { 2 | CONNECT: 'zmq::socket::connect', 3 | RECONNECT: 'zmq::socket::reconnect', 4 | RECONNECT_FAILURE: 'zmq::socket:reconnect-failure', 5 | DISCONNECT: 'zmq::socket::disconnect', 6 | CONNECT_DELAY: 'zmq::socket::connect-delay', 7 | CONNECT_RETRY: 'zmq::socket::connect-retry', 8 | LISTEN: 'zmq::socket::listen', 9 | BIND_ERROR: 'zmq::socket::bind-error', 10 | ACCEPT: 'zmq::socket::accept', 11 | ACCEPT_ERROR: 'zmq::socket::accept-error', 12 | CLOSE: 'zmq::socket::close', 13 | CLOSE_ERROR: 'zmq::socket::close-error' 14 | } 15 | 16 | export default SocketEvent 17 | -------------------------------------------------------------------------------- /src/sockets/example/bug.js: -------------------------------------------------------------------------------- 1 | import zmq from 'zeromq' 2 | 3 | zmq.Context.setMaxThreads(8) 4 | 5 | let getMaxThreads = zmq.Context.getMaxThreads() 6 | let getMaxSockets = zmq.Context.getMaxSockets() 7 | 8 | console.log('getMaxThreads', getMaxThreads) 9 | console.log('getMaxSockets', getMaxSockets) 10 | 11 | let dealer1 = zmq.socket('dealer') 12 | let dealer2 = zmq.socket('dealer') 13 | 14 | let router1 = zmq.socket('router') 15 | let router2 = zmq.socket('router') 16 | 17 | // ** BUG scenario 18 | // router1.monitor(10, 0) 19 | // router2.monitor(10, 0) 20 | // dealer1.monitor(10, 0) 21 | // dealer2.monitor(10, 0) 22 | 23 | let ADDRESS1 = 'tcp://127.0.0.1:5080' 24 | let ADDRESS2 = 'tcp://127.0.0.1:5081' 25 | 26 | router1.bindSync(ADDRESS1) 27 | router2.bindSync(ADDRESS2) 28 | 29 | // ** FIX scenario 30 | router1.monitor(10, 0) 31 | router2.monitor(10, 0) 32 | 33 | dealer1.on('connect', () => { 34 | console.log('dealer1 connected') 35 | }) 36 | dealer2.on('connect', () => { 37 | console.log('dealer2 connected') 38 | }) 39 | 40 | dealer1.connect(ADDRESS1) 41 | 42 | // ** IMPORTANT TO START MONITOR dealer1.monitor(10, 0) 43 | dealer2.connect(ADDRESS2) 44 | 45 | // ** FIX scenario 46 | dealer1.monitor(10, 0) 47 | dealer2.monitor(10, 0) 48 | 49 | router2.on('message', (data) => { 50 | console.log(data) 51 | }) 52 | 53 | setInterval(() => { 54 | // dealer2.send(new Buffer(5)); 55 | }, 1000) 56 | 57 | setTimeout(() => { 58 | console.log(1) 59 | router1.unmonitor() 60 | router1.unbindSync(ADDRESS1) 61 | console.log(2) 62 | setTimeout(() => { 63 | console.log(3) 64 | dealer1.removeAllListeners() 65 | dealer1.unmonitor() 66 | dealer1.disconnect(ADDRESS1) 67 | }, 2000) 68 | }, 3000) 69 | -------------------------------------------------------------------------------- /src/sockets/example/dealer.js: -------------------------------------------------------------------------------- 1 | import { Dealer, SocketEvent } from '../index' 2 | 3 | const runDealer = async () => { 4 | let routerAddress1 = 'tcp://127.0.0.1:5039' 5 | let routerAddress2 = 'tcp://127.0.0.1:5040' 6 | 7 | let dealer1 = new Dealer({ id: 'TestDealer1', options: { layer: 'DealerLayer1' } }) 8 | let dealer2 = new Dealer({ id: 'TestDealer2', options: { layer: 'DealerLayer2' } }) 9 | 10 | dealer1.debugMode(true) 11 | await dealer1.connect(routerAddress1) 12 | await dealer2.connect(routerAddress2) 13 | 14 | dealer1.on(SocketEvent.RECONNECT, () => { console.log('TestDealer1 reconnecting...') }) 15 | dealer1.on(SocketEvent.DISCONNECT, () => { 16 | console.log('TestDealer1 SocketEvent.DISCONNECT') 17 | console.log('TestDealer1 disconnecting') 18 | dealer1.disconnect() 19 | }) 20 | } 21 | 22 | runDealer() 23 | -------------------------------------------------------------------------------- /src/sockets/example/router.js: -------------------------------------------------------------------------------- 1 | import { Router } from '../index' 2 | 3 | const runRouter = async () => { 4 | let bindAddress1 = 'tcp://127.0.0.1:5039' 5 | let bindAddress2 = 'tcp://127.0.0.1:5040' 6 | 7 | let router1 = new Router({ id: 'TestRouter1', options: { layer: 'RouterLayer1' } }) 8 | let router2 = new Router({ id: 'TestRouter2', options: { layer: 'RouterLayer2' } }) 9 | 10 | router1.debugMode(true) 11 | router2.debugMode(true) 12 | 13 | await router1.bind(bindAddress1) 14 | 15 | await router2.bind(bindAddress2) 16 | 17 | // setTimeout(async () => { 18 | // console.log(`Start unbind from ${bindAddress1} .... `) 19 | // await router1.unbind() 20 | // console.log(`Finish unbind from ${bindAddress1} .... `) 21 | // }, 10000) 22 | } 23 | 24 | runRouter() 25 | -------------------------------------------------------------------------------- /src/sockets/example/test.js: -------------------------------------------------------------------------------- 1 | import { Dealer, Router, SocketEvent } from '../index' 2 | 3 | let routerAddress1 = 'tcp://127.0.0.1:5034' 4 | let routerAddress2 = 'tcp://127.0.0.1:5035' 5 | 6 | const runDealer = async () => { 7 | let dealer1 = new Dealer({ id: 'TestDealer1', options: { layer: 'DealerLayer1' } }) 8 | 9 | let dealer2 = new Dealer({ id: 'TestDealer2', options: { layer: 'DealerLayer2' } }) 10 | 11 | dealer1.debugMode(true) 12 | await dealer1.connect(routerAddress1) 13 | await dealer2.connect(routerAddress2) 14 | 15 | dealer1.on(SocketEvent.RECONNECT, () => { console.log('Reconnecting') }) 16 | dealer1.on(SocketEvent.DISCONNECT, () => { 17 | console.log('Dealer 1 SocketEvent.DISCONNECT') 18 | 19 | dealer1.disconnect() 20 | }) 21 | } 22 | 23 | const runRouter = async () => { 24 | let router1 = new Router({ id: 'TestRouter1', options: { layer: 'RouterLayer1' } }) 25 | let router2 = new Router({ id: 'TestRouter2', options: { layer: 'RouterLayer2' } }) 26 | 27 | router1.debugMode(true) 28 | router2.debugMode(true) 29 | 30 | await router1.bind(routerAddress1) 31 | await router2.bind(routerAddress2) 32 | 33 | runDealer() 34 | 35 | setTimeout(async () => { 36 | console.log(`Start unbind from ${routerAddress1} .... `) 37 | await router1.unbind() 38 | console.log(`Finish unbind from ${routerAddress1} .... `) 39 | }, 5000) 40 | } 41 | 42 | runRouter() 43 | -------------------------------------------------------------------------------- /src/sockets/index.js: -------------------------------------------------------------------------------- 1 | export { default as Router } from './router' 2 | export { default as Dealer } from './dealer' 3 | export { default as SocketEvent } from './events' 4 | export { default as Enum } from './enum' 5 | export { default as Watchers } from './watchers' 6 | -------------------------------------------------------------------------------- /src/sockets/router.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by artak on 3/2/17. 3 | */ 4 | import zmq from 'zeromq' 5 | import Promise from 'bluebird' 6 | 7 | import { ZeronodeError, ErrorCodes } from '../errors' 8 | import { Socket } from './socket' 9 | import Envelop from './envelope' 10 | import { EnvelopType } from './enum' 11 | 12 | let _private = new WeakMap() 13 | 14 | export default class RouterSocket extends Socket { 15 | constructor ({ id, options, config } = {}) { 16 | options = options || {} 17 | config = config || {} 18 | 19 | let socket = zmq.socket('router') 20 | 21 | super({ id, socket, options, config }) 22 | 23 | let _scope = { 24 | socket, 25 | bindPromise: null, 26 | bindAddress: null 27 | } 28 | 29 | _private.set(this, _scope) 30 | } 31 | 32 | getAddress () { 33 | let { bindAddress } = _private.get(this) 34 | return bindAddress 35 | } 36 | 37 | setAddress (bindAddress) { 38 | let _scope = _private.get(this) 39 | 40 | if (typeof bindAddress === 'string' && bindAddress.length) { 41 | _scope.bindAddress = bindAddress 42 | } 43 | } 44 | 45 | // ** returns promise 46 | bind (bindAddress) { 47 | if (this.isOnline() && bindAddress === this.getAddress()) { 48 | return Promise.resolve(true) 49 | } 50 | 51 | let _scope = _private.get(this) 52 | let bindPromise = _scope.bindPromise 53 | 54 | if (bindPromise && bindAddress !== this.getAddress()) { 55 | // ** if trying to bind to other address you need to unbind first 56 | let alreadyBindedError = new Error(`Already binded to '${this.getAddress()}', unbind before changing bind address to '${bindAddress}'`) 57 | return Promise.reject(new ZeronodeError({ socketId: this.getId(), code: ErrorCodes.ALREADY_BINDED, error: alreadyBindedError })) 58 | } 59 | 60 | // ** if bind is still pending then returning it 61 | if (bindPromise && bindPromise.isPending() && bindAddress === this.getAddress()) return bindPromise 62 | 63 | if (bindAddress) this.setAddress(bindAddress) 64 | 65 | _scope.bindPromise = new Promise((resolve, reject) => { 66 | let { socket } = _scope 67 | 68 | this.attachSocketMonitor() 69 | 70 | socket.bind(this.getAddress(), (err) => { 71 | if (err) return reject(err) 72 | this.setOnline() 73 | resolve(`Router (${this.getId()}) is binded at address ${this.getAddress()}`) 74 | }) 75 | }) 76 | 77 | return _scope.bindPromise 78 | } 79 | 80 | // ** returns promise 81 | unbind () { 82 | return new Promise((resolve, reject) => { 83 | //* closing and removing all listeners on socket 84 | super.close() 85 | 86 | let _scope = _private.get(this) 87 | let { socket, bindAddress, bindPromise } = _scope 88 | 89 | //* if bind promise is pending then reject it 90 | if (bindPromise && bindPromise.isPending()) { 91 | bindPromise.reject('Unbinding') 92 | } 93 | 94 | _scope.bindPromise = null 95 | 96 | socket.unbindSync(bindAddress) 97 | 98 | this.setOffline() 99 | resolve() 100 | }) 101 | } 102 | 103 | // ** returns promise 104 | async close () { 105 | await this.unbind() 106 | 107 | let { socket } = _private.get(this) 108 | 109 | socket.close() 110 | } 111 | 112 | //* Polymorphic Functions 113 | request ({ to, event, data, timeout, mainEvent = false } = {}) { 114 | let envelop = new Envelop({ type: EnvelopType.REQUEST, tag: event, data, owner: this.getId(), recipient: to, mainEvent }) 115 | return super.request(envelop, timeout) 116 | } 117 | 118 | tick ({ to, event, data, mainEvent = false } = {}) { 119 | let envelop = new Envelop({ type: EnvelopType.TICK, tag: event, data: data, owner: this.getId(), recipient: to, mainEvent }) 120 | return super.tick(envelop) 121 | } 122 | 123 | getSocketMsg (envelop) { 124 | return [envelop.getRecipient(), '', envelop.getBuffer()] 125 | } 126 | } 127 | -------------------------------------------------------------------------------- /src/sockets/socket.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore' 2 | import animal from 'animal-id' 3 | import EventEmitter from 'pattern-emitter' 4 | 5 | import { ZeronodeError, ErrorCodes } from '../errors' 6 | 7 | import SocketEvent from './events' 8 | import Envelop from './envelope' 9 | import { EnvelopType, MetricType, Timeouts } from './enum' 10 | import Watchers from './watchers' 11 | 12 | let _private = new WeakMap() 13 | 14 | function _calculateLatency ({ sendTime, getTime, replyTime, replyGetTime }) { 15 | let processTime = (replyTime[0] * 10e9 + replyTime[1]) - (getTime[0] * 10e9 + getTime[1]) 16 | let requestTime = (replyGetTime[0] * 10e9 + replyGetTime[1]) - (sendTime[0] * 10e9 + sendTime[1]) 17 | 18 | return { 19 | process: processTime, 20 | latency: requestTime - processTime 21 | } 22 | } 23 | 24 | const nop = () => {} 25 | 26 | /** 27 | * 28 | * @param envelop: Object 29 | * @param type: Enum(-1 = timeout, 0 = send, 1 = got) 30 | */ 31 | function emitMetric (envelop, type = 0) { 32 | let event = '' 33 | 34 | if (envelop.mainEvent) return 35 | 36 | switch (envelop.type) { 37 | case EnvelopType.TICK: 38 | event = !type ? MetricType.SEND_TICK : MetricType.GOT_TICK 39 | break 40 | case EnvelopType.REQUEST: 41 | if (type === -1) { 42 | event = MetricType.REQUEST_TIMEOUT 43 | break 44 | } 45 | event = !type ? MetricType.SEND_REQUEST : MetricType.GOT_REQUEST 46 | break 47 | case EnvelopType.RESPONSE: 48 | event = !type ? MetricType.SEND_REPLY_SUCCESS : MetricType.GOT_REPLY_SUCCESS 49 | break 50 | case EnvelopType.ERROR: 51 | event = !type ? MetricType.SEND_REPLY_ERROR : MetricType.GOT_REPLY_ERROR 52 | } 53 | 54 | this.emit(event, envelop) 55 | } 56 | 57 | function buildSocketEventHandler (eventName) { 58 | const handler = (fd, endpoint) => { 59 | if (this.debugMode()) { 60 | this.logger.info(`Emitted '${eventName}' on socket '${this.getId()}'`) 61 | } 62 | this.emit(eventName, { fd, endpoint }) 63 | } 64 | 65 | return this::handler 66 | } 67 | 68 | class Socket extends EventEmitter { 69 | static generateSocketId () { 70 | return animal.getId() 71 | } 72 | 73 | constructor ({ id, socket, config, options } = {}) { 74 | super() 75 | options = options || {} 76 | config = config || {} 77 | 78 | // ** creating the socket 79 | let socketId = id || Socket.generateSocketId() 80 | socket.identity = socketId 81 | socket.on('message', this::onSocketMessage) 82 | 83 | let _scope = { 84 | id: socketId, 85 | socket, 86 | config, 87 | options, 88 | logger: null, 89 | online: false, 90 | metric: nop, 91 | isDebugMode: false, 92 | monitorRestartInterval: null, 93 | requests: new Map(), 94 | requestWatcherMap: { 95 | main: new Map(), 96 | custom: new Map() 97 | }, 98 | tickEmitter: { 99 | main: new EventEmitter(), 100 | custom: new EventEmitter() 101 | } 102 | } 103 | 104 | _private.set(this, _scope) 105 | 106 | // ** setting the logger as soon as possible 107 | this.setLogger(config.logger) 108 | 109 | this.debugMode(false) 110 | } 111 | 112 | getId () { 113 | let { id } = _private.get(this) 114 | return id 115 | } 116 | 117 | setOnline () { 118 | let _scope = _private.get(this) 119 | _scope.online = Date.now() 120 | } 121 | 122 | setOffline () { 123 | let _scope = _private.get(this) 124 | _scope.online = false 125 | } 126 | 127 | isOnline () { 128 | let { online } = _private.get(this) 129 | return !!online 130 | } 131 | 132 | setOptions (options = {}) { 133 | let _scope = _private.get(this) 134 | _scope.options = options 135 | } 136 | 137 | getOptions () { 138 | let { options } = _private.get(this) 139 | return options 140 | } 141 | 142 | getConfig () { 143 | let { config } = _private.get(this) 144 | return config 145 | } 146 | 147 | setMetric (status) { 148 | let _scope = _private.get(this) 149 | _scope.metric = status ? this::emitMetric : nop 150 | } 151 | 152 | setLogger (logger) { 153 | this.logger = logger || console 154 | } 155 | 156 | debugMode (val) { 157 | let _scope = _private.get(this) 158 | if (val) { 159 | _scope.isDebugMode = !!val 160 | } else { 161 | return _scope.isDebugMode 162 | } 163 | } 164 | 165 | request (envelop, reqTimeout) { 166 | let { id, requests, metric, config } = _private.get(this) 167 | reqTimeout = reqTimeout || config.REQUEST_TIMEOUT || Timeouts.REQUEST_TIMEOUT 168 | 169 | if (!this.isOnline()) { 170 | let err = new Error(`Sending failed as socket '${this.getId()}' is not online`) 171 | return Promise.reject(new ZeronodeError({ socketId: id, error: err, code: ErrorCodes.SOCKET_ISNOT_ONLINE })) 172 | } 173 | 174 | let envelopId = envelop.getId() 175 | 176 | return new Promise((resolve, reject) => { 177 | let timeout = setTimeout(() => { 178 | if (requests.has(envelopId)) { 179 | let requestObj = requests.get(envelopId) 180 | requests.delete(envelopId) 181 | 182 | metric(envelop.toJSON(), -1) 183 | 184 | let requestTimeoutedError = new Error(`Request envelop '${envelopId}' timeouted on socket '${this.getId()}'`) 185 | requestObj.reject(new ZeronodeError({ socketId: this.getId(), envelopId: envelopId, error: requestTimeoutedError, code: ErrorCodes.REQUEST_TIMEOUTED })) 186 | } 187 | }, reqTimeout) 188 | 189 | requests.set(envelopId, { resolve: resolve, reject: reject, timeout: timeout, sendTime: process.hrtime() }) 190 | this.sendEnvelop(envelop) 191 | }) 192 | } 193 | 194 | tick (envelop) { 195 | let socketId = this.getId() 196 | if (!this.isOnline()) { 197 | let socketNotOnlineError = new Error(`Sending failed as socket ${socketId} is not online`) 198 | throw new ZeronodeError({ socketId, error: socketNotOnlineError, code: ErrorCodes.SOCKET_ISNOT_ONLINE }) 199 | } 200 | 201 | this.sendEnvelop(envelop) 202 | } 203 | 204 | sendEnvelop (envelop) { 205 | let { socket, metric } = _private.get(this) 206 | let msg = this.getSocketMsg(envelop) 207 | let envelopJSON = envelop.toJSON() 208 | 209 | if (msg instanceof Buffer) { 210 | envelopJSON.size = msg.length 211 | } else { 212 | envelopJSON.size = msg[2].length 213 | } 214 | 215 | metric(envelopJSON) 216 | 217 | socket.send(msg) 218 | } 219 | 220 | attachSocketMonitor () { 221 | let _scope = _private.get(this) 222 | let { config, socket } = _scope 223 | 224 | // ** start monitoring socket events 225 | let monitorTimeout = config.MONITOR_TIMEOUT || Timeouts.MONITOR_TIMEOUT 226 | let monitorRestartTimeout = config.MONITOR_RESTART_TIMEOUT || Timeouts.MONITOR_RESTART_TIMEOUT 227 | 228 | // ** start socket monitoring 229 | socket.monitor(monitorTimeout, 0) 230 | 231 | // ** Handle monitor error and restart it 232 | socket.on('monitor_error', () => { 233 | this.logger.warn(`Restarting monitor after ${monitorRestartTimeout} on socket ${this.getId()}`) 234 | _scope.monitorRestartInterval = setTimeout(() => socket.monitor(monitorTimeout, 0), monitorRestartTimeout) 235 | }) 236 | 237 | socket.on('connect', this::buildSocketEventHandler(SocketEvent.CONNECT)) 238 | socket.on('disconnect', this::buildSocketEventHandler(SocketEvent.DISCONNECT)) 239 | socket.on('connect_delay', this::buildSocketEventHandler(SocketEvent.CONNECT_DELAY)) 240 | socket.on('connect_retry', this::buildSocketEventHandler(SocketEvent.CONNECT_RETRY)) 241 | socket.on('listen', this::buildSocketEventHandler(SocketEvent.LISTEN)) 242 | socket.on('bind_error', this::buildSocketEventHandler(SocketEvent.BIND_ERROR)) 243 | socket.on('accept', this::buildSocketEventHandler(SocketEvent.ACCEPT)) 244 | socket.on('accept_error', this::buildSocketEventHandler(SocketEvent.ACCEPT_ERROR)) 245 | socket.on('close', this::buildSocketEventHandler(SocketEvent.CLOSE)) 246 | socket.on('close_error', this::buildSocketEventHandler(SocketEvent.CLOSE_ERROR)) 247 | } 248 | 249 | detachSocketMonitor () { 250 | let { socket, monitorRestartInterval } = _private.get(this) 251 | // ** remove all listeners 252 | socket.removeAllListeners('connect') 253 | socket.removeAllListeners('disconnect') 254 | socket.removeAllListeners('connect_delay') 255 | socket.removeAllListeners('connect_retry') 256 | socket.removeAllListeners('listen') 257 | socket.removeAllListeners('bind_error') 258 | socket.removeAllListeners('accept') 259 | socket.removeAllListeners('accept_error') 260 | socket.removeAllListeners('close') 261 | socket.removeAllListeners('close_error') 262 | 263 | // ** if during closing there is a monitor restart scheduled then clear the schedule 264 | if (monitorRestartInterval) clearInterval(monitorRestartInterval) 265 | socket.unmonitor() 266 | } 267 | 268 | close () { 269 | this.detachSocketMonitor() 270 | } 271 | 272 | onRequest (endpoint, fn, main = false) { 273 | // ** function will called with argument request = {body, reply} 274 | if (!(endpoint instanceof RegExp)) { 275 | endpoint = endpoint.toString() 276 | } 277 | let { requestWatcherMap } = _private.get(this) 278 | let watcherMap = main ? requestWatcherMap.main : requestWatcherMap.custom 279 | 280 | let requestWatcher = watcherMap.get(endpoint) 281 | 282 | if (!requestWatcher) { 283 | requestWatcher = new Watchers(endpoint) 284 | watcherMap.set(endpoint, requestWatcher) 285 | } 286 | 287 | requestWatcher.addFn(fn) 288 | } 289 | 290 | offRequest (endpoint, fn, main = false) { 291 | let { requestWatcherMap } = _private.get(this) 292 | let watcherMap = main ? requestWatcherMap.main : requestWatcherMap.custom 293 | 294 | if (_.isFunction(fn)) { 295 | let endpointWatcher = watcherMap.get(endpoint) 296 | if (!endpointWatcher) return 297 | endpointWatcher.removeFn(fn) 298 | return 299 | } 300 | 301 | watcherMap.delete(endpoint) 302 | } 303 | 304 | onTick (event, fn, main = false) { 305 | let { tickEmitter } = _private.get(this) 306 | main ? tickEmitter.main.on(event, fn) : tickEmitter.custom.on(event, fn) 307 | } 308 | 309 | offTick (event, fn, main = false) { 310 | let { tickEmitter } = _private.get(this) 311 | let eventTickEmitter = main ? tickEmitter.main : tickEmitter.custom 312 | 313 | if (_.isFunction(fn)) { 314 | eventTickEmitter.removeListener(event, fn) 315 | return 316 | } 317 | 318 | eventTickEmitter.removeAllListeners(event) 319 | } 320 | } 321 | 322 | //* * Handlers of specific envelop msg-es 323 | 324 | //* * when socket is dealer identity is empty 325 | //* * when socket is router, identity is the dealer which sends data 326 | function onSocketMessage (empty, envelopBuffer) { 327 | let { metric, tickEmitter } = _private.get(this) 328 | 329 | let { type, id, owner, recipient, tag, mainEvent } = Envelop.readMetaFromBuffer(envelopBuffer) 330 | let envelop = new Envelop({ type, id, owner, recipient, tag, mainEvent }) 331 | let envelopData = Envelop.readDataFromBuffer(envelopBuffer) 332 | envelop.setData(envelopData) 333 | 334 | let envelopJSON = envelop.toJSON() 335 | envelopJSON.size = envelopBuffer.length 336 | 337 | switch (type) { 338 | case EnvelopType.TICK: 339 | metric(envelopJSON, 1) 340 | 341 | if (mainEvent) { 342 | tickEmitter.main.emit(tag, envelopData) 343 | } else { 344 | tickEmitter.custom.emit(tag, envelopData, { 345 | id: owner, 346 | event: tag 347 | }) 348 | } 349 | break 350 | case EnvelopType.REQUEST: 351 | metric(envelopJSON, 1) 352 | // ** if metric is enabled then emit it 353 | this::syncEnvelopHandler(envelop) 354 | break 355 | case EnvelopType.RESPONSE: 356 | case EnvelopType.ERROR: 357 | envelop.size = envelopBuffer.length 358 | this::responseEnvelopHandler(envelop) 359 | break 360 | } 361 | } 362 | 363 | function syncEnvelopHandler (envelop) { 364 | let self = this 365 | let getTime = process.hrtime() 366 | 367 | let prevOwner = envelop.getOwner() 368 | let handlers = self::determineHandlersByTag(envelop.getTag(), envelop.isMain()) 369 | 370 | if (!handlers.length) return 371 | 372 | let requestOb = { 373 | head: { 374 | id: envelop.getOwner(), 375 | event: envelop.getTag() 376 | }, 377 | body: envelop.getData(), 378 | reply: (response) => { 379 | envelop.setRecipient(prevOwner) 380 | envelop.setOwner(self.getId()) 381 | envelop.setType(EnvelopType.RESPONSE) 382 | envelop.setData({ getTime, replyTime: process.hrtime(), data: response }) 383 | self.sendEnvelop(envelop) 384 | }, 385 | error: (err) => { 386 | envelop.setRecipient(prevOwner) 387 | envelop.setOwner(self.getId()) 388 | envelop.setType(EnvelopType.ERROR) 389 | envelop.setData({ getTime, replyTime: process.hrtime(), data: err }) 390 | 391 | self.sendEnvelop(envelop) 392 | }, 393 | next: (err) => { 394 | if (err) { 395 | return requestOb.error(err) 396 | } 397 | 398 | if (!handlers.length) { 399 | let noHandlerErr = new Error(`There is no handlers available as to process next() on socket '${self.getId()}'`) 400 | throw new ZeronodeError({ socketId: self.getId(), code: ErrorCodes.NO_NEXT_HANDLER_AVAILABLE, error: noHandlerErr }) 401 | } 402 | 403 | handlers.shift()(requestOb) 404 | } 405 | } 406 | 407 | handlers.shift()(requestOb) 408 | } 409 | 410 | function determineHandlersByTag (tag, main = false) { 411 | let handlers = [] 412 | 413 | let { requestWatcherMap } = _private.get(this) 414 | let watcherMap = main ? requestWatcherMap.main : requestWatcherMap.custom 415 | 416 | for (let endpoint of watcherMap.keys()) { 417 | if (endpoint instanceof RegExp) { 418 | if (endpoint.test(tag)) { 419 | watcherMap.get(endpoint).getFnMap().forEach((index, fnKey) => { 420 | handlers.push({ index, fnKey }) 421 | }) 422 | } 423 | } else if (endpoint === tag) { 424 | watcherMap.get(endpoint).getFnMap().forEach((index, fnKey) => { 425 | handlers.push({ index, fnKey }) 426 | }) 427 | } 428 | } 429 | 430 | return handlers.sort((a, b) => { 431 | return a.index - b.index 432 | }).map((ob) => ob.fnKey) 433 | } 434 | 435 | function responseEnvelopHandler (envelop) { 436 | let { requests, metric } = _private.get(this) 437 | 438 | let id = envelop.getId() 439 | if (!requests.has(id)) { 440 | // ** TODO:: metric 441 | return this.logger.warn(`Response ${id} is probably time outed`) 442 | } 443 | 444 | //* * requestObj is like {resolve, reject, timeout : clearRequestTimeout} 445 | let { timeout, sendTime, resolve, reject } = requests.get(id) 446 | 447 | // ** getTime is the time when message arrives to server 448 | // ** replyTime is the time when message is send from server 449 | let gotReplyMetric = envelop.toJSON() 450 | let { getTime, replyTime } = gotReplyMetric.data 451 | let duration = _calculateLatency({ sendTime, getTime, replyTime, replyGetTime: process.hrtime() }) 452 | 453 | gotReplyMetric.data = { 454 | data: gotReplyMetric.data, 455 | duration 456 | } 457 | 458 | gotReplyMetric.size = envelop.size 459 | 460 | metric(gotReplyMetric, 1) 461 | 462 | clearTimeout(timeout) 463 | requests.delete(id) 464 | 465 | let { data } = envelop.getData() 466 | //* * resolving request promise with response data 467 | envelop.getType() === EnvelopType.ERROR ? reject(data) : resolve(data) 468 | } 469 | 470 | // ** exports 471 | export { SocketEvent } 472 | export { Socket } 473 | 474 | export default { 475 | SocketEvent, 476 | Socket 477 | } 478 | -------------------------------------------------------------------------------- /src/sockets/watchers.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by avar on 7/11/17. 3 | */ 4 | import { isFunction } from 'underscore' 5 | 6 | let index = 1 7 | 8 | export default class Watchers { 9 | constructor (tag) { 10 | this._tag = tag 11 | this._fnMap = new Map() 12 | } 13 | 14 | getFnMap () { 15 | return this._fnMap 16 | } 17 | 18 | addFn (fn) { 19 | if (isFunction(fn)) { 20 | this._fnMap.set(fn, index) 21 | index++ 22 | } 23 | } 24 | 25 | removeFn (fn) { 26 | if (isFunction(fn)) { 27 | this._fnMap.delete(fn) 28 | return 29 | } 30 | 31 | this._fnMap.clear() 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | import _ from 'underscore' 2 | 3 | // ** we use this to check if node is satisfying the predicate and adding node id into accumulatorSet 4 | const checkNodeReducer = (node, predicate, accumulatorSet) => { 5 | let nodeOptions = node.getOptions() 6 | if (_.isFunction(predicate) && predicate(nodeOptions)) { 7 | accumulatorSet.add(node.getId()) 8 | } 9 | } 10 | 11 | const optionsPredicateBuilder = (options) => { 12 | return (nodeOptions) => { 13 | let optionsKeysArray = Object.keys(options) 14 | let notsatisfying = _.find(optionsKeysArray, (optionKey) => { 15 | let optionValue = options[optionKey] 16 | // ** which could also not exist 17 | let nodeOptionValue = nodeOptions[optionKey] 18 | 19 | if (nodeOptionValue) { 20 | if (_.isRegExp(optionValue)) { 21 | return !optionValue.test(nodeOptionValue) 22 | } 23 | 24 | if (_.isString(optionValue) || _.isNumber(optionValue)) { 25 | return optionValue !== nodeOptionValue 26 | } 27 | 28 | if (_.isObject(optionValue)) { 29 | return !!_.find(optionValue, (value, operator) => { 30 | switch (operator) { 31 | case '$eq': 32 | return value !== nodeOptionValue 33 | case '$ne': 34 | return value === nodeOptionValue 35 | case '$aeq': 36 | return value != nodeOptionValue 37 | case '$gt': 38 | return value >= nodeOptionValue 39 | case '$gte': 40 | return value > nodeOptionValue 41 | case '$lt': 42 | return value <= nodeOptionValue 43 | case '$lte': 44 | return value < nodeOptionValue 45 | case '$between': 46 | return value[0] >= nodeOptionValue || value[1] <= nodeOptionValue 47 | case '$regex': 48 | return !value.test(nodeOptionValue) 49 | case '$in': 50 | return value.indexOf(nodeOptionValue) === -1 51 | case '$nin': 52 | return value.indexOf(nodeOptionValue) !== -1 53 | case '$contains': 54 | return nodeOptionValue.indexOf(value) === -1 55 | case '$containsAny': 56 | return !_.find(value, (v) => nodeOptionValue.indexOf(v) !== -1) 57 | case '$containsNone': 58 | return !!_.find(value, (v) => nodeOptionValue.indexOf(v) !== -1) 59 | } 60 | }) 61 | } 62 | } 63 | 64 | return true 65 | }) 66 | 67 | return !notsatisfying 68 | } 69 | } 70 | 71 | export default { 72 | checkNodeReducer, 73 | optionsPredicateBuilder 74 | } 75 | -------------------------------------------------------------------------------- /test/client-server.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | import Client from '../src/client' 3 | import Server from '../src/server' 4 | 5 | const address = 'tcp://127.0.0.1:5001' 6 | 7 | describe('Client/Server', () => { 8 | let client, server 9 | 10 | beforeEach((done) => { 11 | client = new Client({}) 12 | server = new Server({}) 13 | done() 14 | }) 15 | 16 | afterEach(async () => { 17 | await client.close() 18 | await server.close() 19 | client = null 20 | server = null 21 | }) 22 | 23 | it('tickToServer', done => { 24 | let expectedMessage = 'xndzor' 25 | server.bind(address) 26 | .then(() => { 27 | return client.connect(address) 28 | }) 29 | .then(() => { 30 | server.onTick('tandz', (message) => { 31 | assert.equal(message, expectedMessage) 32 | done() 33 | }) 34 | client.tick({ event: 'tandz', data: expectedMessage }) 35 | }) 36 | }) 37 | 38 | it('requesttoServer-timeout', done => { 39 | let expectedMessage = 'xndzor' 40 | server.bind(address) 41 | .then(() => { 42 | return client.connect(address) 43 | }) 44 | .then(() => { 45 | return client.request({ event: 'tandz', data: expectedMessage, timeout: 500 }) 46 | }) 47 | .catch(err => { 48 | assert.include(err.message, 'timeouted') 49 | done() 50 | }) 51 | }) 52 | 53 | it('requestToServer-response', done => { 54 | let expectedMessage = 'xndzor' 55 | server.bind(address) 56 | .then(() => { 57 | return client.connect(address) 58 | }) 59 | .then(() => { 60 | server.onRequest('tandz', ({body, reply}) => { 61 | assert.equal(body, expectedMessage) 62 | reply(expectedMessage) 63 | }) 64 | return client.request({ event: 'tandz', data: expectedMessage, timeout: 2000 }) 65 | }) 66 | .then((message) => { 67 | assert.equal(message, expectedMessage) 68 | done() 69 | }) 70 | }) 71 | 72 | it('tickToClient', done => { 73 | let expectedMessage = 'xndzor' 74 | server.bind(address) 75 | .then(() => { 76 | return client.connect(address) 77 | }) 78 | .then(() => { 79 | client.onTick('tandz', message => { 80 | assert.equal(message, expectedMessage) 81 | done() 82 | }) 83 | server.tick({ to: client.getId(), event: 'tandz', data: expectedMessage }) 84 | }) 85 | }) 86 | 87 | it('requestToClient-timeout', done => { 88 | let expectedMessage = 'xndzor' 89 | server.bind(address) 90 | .then(() => { 91 | return client.connect(address) 92 | }) 93 | .then(() => { 94 | return server.request({ to: client.getId(), event: 'tandz', data: expectedMessage, timeout: 500 }) 95 | }) 96 | .catch(err => { 97 | assert.include(err.message, 'timeouted') 98 | done() 99 | }) 100 | }) 101 | 102 | it('requestToClient-response', done => { 103 | let expectedMessage = 'xndzor' 104 | server.bind(address) 105 | .then(() => { 106 | return client.connect(address) 107 | }) 108 | .then(() => { 109 | client.onRequest('tandz', ({body, reply}) => { 110 | assert.equal(body, expectedMessage) 111 | reply(body) 112 | }) 113 | return server.request({ to: client.getId(), event: 'tandz', data: expectedMessage }) 114 | }) 115 | .then(message => { 116 | assert.equal(message, expectedMessage) 117 | done() 118 | }) 119 | }) 120 | }) 121 | -------------------------------------------------------------------------------- /test/manyToMany.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by root on 12/13/17. 3 | */ 4 | import { assert } from 'chai' 5 | import _ from 'underscore' 6 | 7 | import { Node } from '../src' 8 | 9 | describe('manyToMany', () => { 10 | let clients, servers, centreNode 11 | const CLIENTS_COUNT = 10 12 | 13 | beforeEach(async () => { 14 | clients = _.map(_.range(CLIENTS_COUNT), (i) => new Node({ options: {clientName: `client${i}`} })) 15 | servers = _.map(_.range(CLIENTS_COUNT), (i) => new Node({ bind: `tcp://127.0.0.1:301${i}`, options: {serverName: `server${i}`} })) 16 | centreNode = new Node({ bind: 'tcp://127.0.0.1:3000' }) 17 | 18 | await centreNode.bind() 19 | await Promise.all(_.map(servers, async (server) => { 20 | await server.bind() 21 | await centreNode.connect({ address: server.getAddress() }) 22 | })) 23 | await Promise.all(_.map(clients, (client) => client.connect({ address: centreNode.getAddress() }))) 24 | }) 25 | 26 | afterEach(async () => { 27 | await Promise.all(_.map(clients, (client) => client.stop())) 28 | await centreNode.stop() 29 | await Promise.all(_.map(servers, (server) => server.stop())) 30 | clients = null 31 | centreNode = null 32 | servers = null 33 | }) 34 | 35 | it('tickAnyUp', (done) => { 36 | let expectedMessage = 'bar' 37 | 38 | _.each(servers, (server) => { 39 | server.onTick('foo', (message) => { 40 | assert.equal(message, expectedMessage) 41 | done() 42 | }) 43 | }) 44 | 45 | centreNode.tickUpAny({ event: 'foo', data: expectedMessage }) 46 | }) 47 | 48 | it('tickAnyUp', (done) => { 49 | let expectedMessage = 'bar' 50 | 51 | _.each(servers, (server) => { 52 | server.onTick('foo', (message) => { 53 | assert.equal(message, expectedMessage) 54 | done() 55 | }) 56 | }) 57 | 58 | centreNode.tickUpAny({ event: 'foo', data: expectedMessage }) 59 | }) 60 | 61 | it('tickAnyDown', (done) => { 62 | let expectedMessage = 'bar' 63 | 64 | _.each(clients, (client) => { 65 | client.onTick('foo', (message) => { 66 | assert.equal(message, expectedMessage) 67 | done() 68 | }) 69 | }) 70 | 71 | centreNode.tickDownAny({ event: 'foo', data: expectedMessage }) 72 | }) 73 | 74 | it('tickAllUp', (done) => { 75 | let expectedMessage = 'bar' 76 | let count = 0 77 | 78 | _.each(servers, (server) => { 79 | server.onTick('foo', (message) => { 80 | assert.equal(message, expectedMessage) 81 | count++ 82 | count === CLIENTS_COUNT && done() 83 | }) 84 | }) 85 | 86 | centreNode.tickUpAll({ event: 'foo', data: expectedMessage }) 87 | }) 88 | 89 | it('tickAllDown', (done) => { 90 | let expectedMessage = 'bar' 91 | let count = 0 92 | 93 | _.each(clients, (client) => { 94 | client.onTick('foo', (message) => { 95 | assert.equal(message, expectedMessage) 96 | count++ 97 | count === CLIENTS_COUNT && done() 98 | }) 99 | }) 100 | 101 | centreNode.tickDownAll({ event: 'foo', data: expectedMessage }) 102 | }) 103 | 104 | it('requestAnyDown', async () => { 105 | let expectedMessage = 'bar' 106 | let expectedMessage2 = 'baz' 107 | 108 | _.each(clients, (client) => { 109 | client.onRequest('foo', ({ body, reply }) => { 110 | assert.equal(body, expectedMessage) 111 | reply(expectedMessage2) 112 | }) 113 | }) 114 | 115 | let response = await centreNode.requestDownAny({ event: 'foo', data: expectedMessage }) 116 | 117 | assert.equal(expectedMessage2, response) 118 | }) 119 | 120 | it('requestAnyUp', async () => { 121 | let expectedMessage = 'bar' 122 | let expectedMessage2 = 'baz' 123 | 124 | _.each(servers, (server) => { 125 | server.onRequest('foo', ({ body, reply }) => { 126 | assert.equal(body, expectedMessage) 127 | reply(expectedMessage2) 128 | }) 129 | }) 130 | 131 | let response = await centreNode.requestUpAny({ event: 'foo', data: expectedMessage }) 132 | 133 | assert.equal(expectedMessage2, response) 134 | }) 135 | }) 136 | -------------------------------------------------------------------------------- /test/manyToOne.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by root on 12/13/17. 3 | */ 4 | import { assert } from 'chai' 5 | import _ from 'underscore' 6 | 7 | import { Node } from '../src' 8 | 9 | describe('manyToOne', () => { 10 | let clients, serverNode 11 | const CLIENTS_COUNT = 10 12 | 13 | beforeEach(async () => { 14 | clients = _.map(_.range(CLIENTS_COUNT), (i) => new Node({ options: {clientName: `client${i}`, idx: [i]} })) 15 | serverNode = new Node({ bind: 'tcp://127.0.0.1:3000' }) 16 | await serverNode.bind() 17 | }) 18 | 19 | afterEach(async () => { 20 | await Promise.all(_.map(clients, (client) => client.stop())) 21 | await serverNode.stop() 22 | clients = null 23 | serverNode = null 24 | }) 25 | 26 | it('tickFromClients', (done) => { 27 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 28 | .then(() => { 29 | let expectedMessage = 'bar' 30 | let count = 0 31 | 32 | serverNode.onTick('foo', (message) => { 33 | assert.equal(message, expectedMessage) 34 | count++ 35 | if (count === CLIENTS_COUNT) done() 36 | }) 37 | 38 | _.each(clients, (client) => client.tick({to: serverNode.getId(), event: 'foo', data: expectedMessage})) 39 | }) 40 | }) 41 | 42 | it('tickAnyFromServer-string', (done) => { 43 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 44 | .then(() => { 45 | let expectedMessage = 'bar' 46 | clients[2].onTick('foo', (data) => { 47 | assert.equal(data, expectedMessage) 48 | done() 49 | }) 50 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: 'client2'}}) 51 | }) 52 | }) 53 | 54 | it('tickAnyFromServer-object-eq', (done) => { 55 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 56 | .then(() => { 57 | let expectedMessage = 'bar' 58 | clients[2].onTick('foo', (data) => { 59 | assert.equal(data, expectedMessage) 60 | done() 61 | }) 62 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $eq: 'client2' }}}) 63 | }) 64 | }) 65 | 66 | it('tickAnyFromServer-object-aeq', (done) => { 67 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 68 | .then(() => { 69 | let expectedMessage = 'bar' 70 | clients[2].onTick('foo', (data) => { 71 | assert.equal(data, expectedMessage) 72 | done() 73 | }) 74 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $aeq: 'client2' }}}) 75 | }) 76 | }) 77 | 78 | it('tickAnyFromServer-object-gt', (done) => { 79 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 80 | .then(() => { 81 | let expectedMessage = 'bar' 82 | clients[9].onTick('foo', (data) => { 83 | assert.equal(data, expectedMessage) 84 | done() 85 | }) 86 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $gt: 'client8' }}}) 87 | }) 88 | }) 89 | 90 | it('tickAnyFromServer-object-gte', (done) => { 91 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 92 | .then(() => { 93 | let expectedMessage = 'bar' 94 | clients[9].onTick('foo', (data) => { 95 | assert.equal(data, expectedMessage) 96 | done() 97 | }) 98 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $gte: 'client9' }}}) 99 | }) 100 | }) 101 | 102 | it('tickAnyFromServer-object-lt', (done) => { 103 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 104 | .then(() => { 105 | let expectedMessage = 'bar' 106 | clients[0].onTick('foo', (data) => { 107 | assert.equal(data, expectedMessage) 108 | done() 109 | }) 110 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $lt: 'client1' }}}) 111 | }) 112 | }) 113 | 114 | it('tickAnyFromServer-object-lte', (done) => { 115 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 116 | .then(() => { 117 | let expectedMessage = 'bar' 118 | clients[0].onTick('foo', (data) => { 119 | assert.equal(data, expectedMessage) 120 | done() 121 | }) 122 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $lte: 'client0' }}}) 123 | }) 124 | }) 125 | 126 | it('tickAnyFromServer-object-between', (done) => { 127 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 128 | .then(() => { 129 | let expectedMessage = 'bar' 130 | clients[2].onTick('foo', (data) => { 131 | assert.equal(data, expectedMessage) 132 | done() 133 | }) 134 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $between: ['client1', 'client3'] }}}) 135 | }) 136 | }) 137 | 138 | it('tickAnyFromServer-object-in', (done) => { 139 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 140 | .then(() => { 141 | let expectedMessage = 'bar' 142 | clients[2].onTick('foo', (data) => { 143 | assert.equal(data, expectedMessage) 144 | done() 145 | }) 146 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $in: ['client2'] }}}) 147 | }) 148 | }) 149 | 150 | it('tickAnyFromServer-object-nin', (done) => { 151 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 152 | .then(() => { 153 | let expectedMessage = 'bar' 154 | clients[2].onTick('foo', (data) => { 155 | assert.equal(data, expectedMessage) 156 | done() 157 | }) 158 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $nin: ['client0', 'client1', 'client3', 'client4', 'client5', 'client6', 'client7', 'client8', 'client9'] }}}) 159 | }) 160 | }) 161 | 162 | it('tickAnyFromServer-number-error', (done) => { 163 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 164 | .then(() => { 165 | let expectedMessage = 'bar' 166 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: 1}}) 167 | }) 168 | .catch((err) => { 169 | assert.equal(err.code, 14) 170 | done() 171 | }) 172 | }) 173 | 174 | it('tickAnyFromServer-error', (done) => { 175 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 176 | .then(() => { 177 | let expectedMessage = 'bar' 178 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {name: 1}}) 179 | }) 180 | .catch((err) => { 181 | assert.equal(err.code, 14) 182 | done() 183 | }) 184 | }) 185 | 186 | it('tickAnyFromServer-object-contains', (done) => { 187 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 188 | .then(() => { 189 | let expectedMessage = 'bar' 190 | clients[2].onTick('foo', (data) => { 191 | assert.equal(data, expectedMessage) 192 | done() 193 | }) 194 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {idx: { $contains: 2 }}}) 195 | }) 196 | }) 197 | 198 | it('tickAnyFromServer-object-containsAny', (done) => { 199 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 200 | .then(() => { 201 | let expectedMessage = 'bar' 202 | clients[2].onTick('foo', (data) => { 203 | assert.equal(data, expectedMessage) 204 | done() 205 | }) 206 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {idx: { $containsAny: [2, 100] }}}) 207 | }) 208 | }) 209 | 210 | it('tickAnyFromServer-object-containsNone', (done) => { 211 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 212 | .then(() => { 213 | let expectedMessage = 'bar' 214 | _.find(clients, (client, i) => { 215 | client.onTick('foo', (data) => { 216 | assert.equal(data, expectedMessage) 217 | done() 218 | }) 219 | return i === 5 220 | }) 221 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {idx: { $containsNone: [ 6, 7, 8, 9] }}}) 222 | }) 223 | }) 224 | 225 | it('tickAnyFromServer-object-regexp', (done) => { 226 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 227 | .then(() => { 228 | let expectedMessage = 'bar' 229 | _.find(clients, (client, i) => { 230 | client.onTick('foo', (data) => { 231 | assert.equal(data, expectedMessage) 232 | done() 233 | }) 234 | return i === 5 235 | }) 236 | 237 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: { $regex: /client[0-5]/ }}}) 238 | }) 239 | }) 240 | 241 | it('tickAnyFromServer-regexp', (done) => { 242 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 243 | .then(() => { 244 | let expectedMessage = 'bar' 245 | _.find(clients, (client, i) => { 246 | client.onTick('foo', (data) => { 247 | assert.equal(data, expectedMessage) 248 | done() 249 | }) 250 | return i === 5 251 | }) 252 | 253 | serverNode.tickAny({event: 'foo', data: expectedMessage, filter: {clientName: /client[0-5]/}}) 254 | }) 255 | }) 256 | 257 | it('tickAnyFromServer-function', (done) => { 258 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 259 | .then(() => { 260 | let expectedMessage = 'bar' 261 | _.each(clients, (client, i) => { 262 | if (i % 2) { 263 | client.onTick('foo', (data) => { 264 | assert.equal(data, expectedMessage) 265 | done() 266 | }) 267 | } 268 | }) 269 | 270 | serverNode.tickAny({ event: 'foo', data: expectedMessage, filter: _predicate }) 271 | }) 272 | }) 273 | 274 | it('tickAllFromServer-string', (done) => { 275 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 276 | .then(() => { 277 | let expectedMessage = 'bar' 278 | clients[2].onTick('foo', (data) => { 279 | assert.equal(data, expectedMessage) 280 | done() 281 | }) 282 | serverNode.tickAll({event: 'foo', data: expectedMessage, filter: {clientName: 'client2'}}) 283 | }) 284 | }) 285 | 286 | it('tickAllFromServer-object-ne', () => { 287 | return Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 288 | .then(() => { 289 | let expectedMessage = 'bar' 290 | 291 | let p = Promise.all(_.map(clients, (client) => { 292 | if (client.getOptions().clientName === 'client1') return Promise.resolve() 293 | return new Promise((resolve, reject) => { 294 | client.onTick('foo', (data) => { 295 | assert.equal(data, expectedMessage) 296 | resolve() 297 | }) 298 | }) 299 | })) 300 | serverNode.tickAll({event: 'foo', data: expectedMessage, filter: {clientName: { $ne: 'client1' }}}) 301 | return p 302 | }) 303 | }) 304 | 305 | it('tickAllFromServer-regexp', (done) => { 306 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 307 | .then(() => { 308 | let expectedMessage = 'bar' 309 | let count = 0 310 | 311 | _.find(clients, (client, i) => { 312 | client.onTick('foo', (data) => { 313 | assert.equal(data, expectedMessage) 314 | count++ 315 | count === 6 && done() 316 | }) 317 | return i === 5 318 | }) 319 | 320 | serverNode.tickAll({event: 'foo', data: expectedMessage, filter: {clientName: /client[0-5]/}}) 321 | }) 322 | }) 323 | 324 | it('tickAllFromServer-function', (done) => { 325 | Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 326 | .then(() => { 327 | let expectedMessage = 'bar' 328 | let count = 0 329 | 330 | _.each(clients, (client, i) => { 331 | if (i % 2) { 332 | client.onTick('foo', (data) => { 333 | assert.equal(data, expectedMessage) 334 | count++ 335 | count === 5 && done() 336 | }) 337 | } 338 | }) 339 | 340 | serverNode.tickAll({ event: 'foo', data: expectedMessage, filter: _predicate }) 341 | }) 342 | }) 343 | 344 | it('requestAnyFromServer', async () => { 345 | await Promise.all(_.map(clients, (client) => client.connect({ address: serverNode.getAddress() }))) 346 | 347 | let expectedMessage = 'bar' 348 | let expectedMessage2 = 'baz' 349 | _.each(clients, (client) => { 350 | client.onRequest('foo', ({ body, reply }) => { 351 | assert.equal(body, expectedMessage) 352 | reply(expectedMessage2) 353 | }) 354 | }) 355 | 356 | let response = await serverNode.requestAny({ event: 'foo', data: expectedMessage }) 357 | 358 | assert.equal(response, expectedMessage2) 359 | }) 360 | }) 361 | 362 | function _predicate (options) { 363 | let clientNumber = parseInt(options.clientName[options.clientName.length - 1]) 364 | 365 | return clientNumber % 2 366 | } 367 | -------------------------------------------------------------------------------- /test/metrics.js: -------------------------------------------------------------------------------- 1 | import { assert } from 'chai' 2 | 3 | import { Node, MetricEvents, ErrorCodes } from '../src' 4 | 5 | describe('metrics', () => { 6 | let clientNode, serverNode 7 | 8 | beforeEach(async() => { 9 | clientNode = new Node({}) 10 | serverNode = new Node({bind: 'tcp://127.0.0.1:3000'}) 11 | await serverNode.bind() 12 | await clientNode.connect({ address: serverNode.getAddress() }) 13 | }) 14 | 15 | afterEach(async() => { 16 | await clientNode.stop() 17 | await serverNode.stop() 18 | clientNode = null 19 | serverNode = null 20 | }) 21 | 22 | it('tick metrics', (done) => { 23 | clientNode.on(MetricEvents.SEND_TICK, (data) => { 24 | assert.equal(data.owner, clientNode.getId()) 25 | assert.equal(data.recipient, serverNode.getId()) 26 | done() 27 | }) 28 | clientNode.enableMetrics(100) 29 | serverNode.enableMetrics() 30 | clientNode.tickAny({ event: 'foo', data: 'bar' }) 31 | }) 32 | 33 | it('request metrics', (done) => { 34 | clientNode.on(MetricEvents.SEND_REQUEST, (data) => { 35 | assert.equal(data.owner, clientNode.getId()) 36 | assert.equal(data.recipient, serverNode.getId()) 37 | done() 38 | }) 39 | 40 | clientNode.enableMetrics(100) 41 | serverNode.enableMetrics() 42 | clientNode.requestAny({ event: 'foo', data: 'bar', timeout: 100 }) 43 | .catch((err) => { 44 | // 45 | }) 46 | }) 47 | 48 | it('request-timeout metrics', (done) => { 49 | let id = '' 50 | clientNode.on(MetricEvents.SEND_REQUEST, (data) => { 51 | id = data.id 52 | assert.equal(data.owner, clientNode.getId()) 53 | assert.equal(data.recipient, serverNode.getId()) 54 | }) 55 | 56 | clientNode.on(MetricEvents.REQUEST_TIMEOUT, (data) => { 57 | assert.equal(data.id, id) 58 | done() 59 | }) 60 | clientNode.enableMetrics(100) 61 | serverNode.enableMetrics() 62 | clientNode.requestAny({ event: 'foo', data: 'bar', timeout: 100 }) 63 | .catch((err) => { 64 | // 65 | }) 66 | }) 67 | 68 | it('request-reply metrics', (done) => { 69 | let id = '' 70 | clientNode.on(MetricEvents.SEND_REQUEST, (data) => { 71 | id = data.id 72 | assert.equal(data.owner, clientNode.getId()) 73 | assert.equal(data.recipient, serverNode.getId()) 74 | }) 75 | clientNode.on(MetricEvents.GOT_REPLY_SUCCESS, (data) => { 76 | assert.equal(data.recipient, clientNode.getId()) 77 | assert.equal(data.owner, serverNode.getId()) 78 | assert.equal(data.id, id) 79 | done() 80 | }) 81 | clientNode.enableMetrics(100) 82 | serverNode.enableMetrics() 83 | serverNode.onRequest('foo', ({ reply }) => { 84 | reply('bar') 85 | }) 86 | clientNode.requestAny({ event: 'foo', data: 'bar' }) 87 | .catch((err) => { 88 | // 89 | }) 90 | }) 91 | 92 | it('request-error metrics', (done) => { 93 | let id = '' 94 | clientNode.on(MetricEvents.SEND_REQUEST, (data) => { 95 | id = data.id 96 | assert.equal(data.owner, clientNode.getId()) 97 | assert.equal(data.recipient, serverNode.getId()) 98 | }) 99 | clientNode.on(MetricEvents.GOT_REPLY_ERROR, (data) => { 100 | assert.equal(data.recipient, clientNode.getId()) 101 | assert.equal(data.owner, serverNode.getId()) 102 | assert.equal(id, data.id) 103 | done() 104 | }) 105 | clientNode.enableMetrics(100) 106 | serverNode.enableMetrics() 107 | serverNode.onRequest('foo', ({ error }) => { 108 | error('bar') 109 | }) 110 | clientNode.requestAny({ event: 'foo', data: 'bar' }) 111 | .catch((err) => { 112 | // 113 | }) 114 | }) 115 | }) 116 | -------------------------------------------------------------------------------- /test/oneToOne.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Created by root on 12/13/17. 3 | */ 4 | import { assert } from 'chai' 5 | import { Node, NodeEvents, ErrorCodes } from '../src' 6 | import { Dealer, Router } from '../src/sockets' 7 | 8 | describe('oneToOne, failures', () => { 9 | let clientNode, serverNode 10 | 11 | beforeEach(async () => { 12 | clientNode = new Node({}) 13 | serverNode = new Node({ bind: 'tcp://127.0.0.1:3000' }) 14 | }) 15 | 16 | afterEach(async () => { 17 | await clientNode.stop() 18 | await serverNode.stop() 19 | clientNode = null 20 | serverNode = null 21 | }) 22 | 23 | it('connect wrong argument', async () => { 24 | try { 25 | await clientNode.connect({ address: null }) 26 | } catch (err) { 27 | assert.equal(err.message, `Wrong type for argument address null`) 28 | } 29 | }) 30 | 31 | it('second connect attempt', async () => { 32 | await serverNode.bind() 33 | await clientNode.connect({ address: serverNode.getAddress() }) 34 | await clientNode.connect({ address: serverNode.getAddress() }) 35 | }) 36 | 37 | it('disconnect wrong argument', async () => { 38 | try { 39 | await clientNode.disconnect(null) 40 | } catch (err) { 41 | assert.equal(err.message, `Wrong type for argument address null`) 42 | } 43 | }) 44 | 45 | it('disconnect from not connected address', async () => { 46 | await clientNode.disconnect(serverNode.getAddress()) 47 | }) 48 | 49 | it('connect timeout', async () => { 50 | try { 51 | await clientNode.connect({ address: serverNode.getAddress(), timeout: 1000 }) 52 | } catch (err) { 53 | assert.equal(err.description, `Error while disconnecting client '${clientNode.getId()}'`) 54 | } 55 | }) 56 | 57 | it('request timeout', async () => { 58 | try { 59 | await serverNode.bind() 60 | await clientNode.connect({ address: serverNode.getAddress() }) 61 | await clientNode.request({ to: serverNode.getId(), event: 'foo', data: 'bar', timeout: 200 }) 62 | } catch (err) { 63 | assert.include(err.message, 'timeouted') 64 | } 65 | }) 66 | 67 | it('request after offRequest', async () => { 68 | try { 69 | await serverNode.bind() 70 | await clientNode.connect({ address: serverNode.getAddress() }) 71 | serverNode.onRequest('foo', ({ body, reply }) => { 72 | reply(body) 73 | }) 74 | serverNode.offRequest('foo') 75 | await clientNode.request({ to: serverNode.getId(), event: 'foo', data: 'bar', timeout: 200 }) 76 | return Promise.reject('fail') 77 | } catch (err) { 78 | assert.include(err.message, 'timeouted') 79 | } 80 | }) 81 | 82 | it('request after offRequest(function)', async () => { 83 | try { 84 | await serverNode.bind() 85 | await clientNode.connect({ address: serverNode.getAddress() }) 86 | 87 | let fooListener = ({ body, reply }) => { 88 | reply(body) 89 | } 90 | 91 | serverNode.onRequest('foo', fooListener) 92 | serverNode.offRequest('foo', fooListener) 93 | 94 | await clientNode.request({ to: serverNode.getId(), event: 'foo', data: 'bar', timeout: 200 }) 95 | return Promise.reject('fail') 96 | } catch (err) { 97 | assert.include(err.message, 'timeouted') 98 | } 99 | }) 100 | 101 | it('request after disconnect', async () => { 102 | try { 103 | await serverNode.bind() 104 | await clientNode.connect({ address: serverNode.getAddress() }) 105 | await clientNode.disconnect(serverNode.getAddress()) 106 | await clientNode.request({ to: serverNode.getId(), event: 'foo', data: 'bar' }) 107 | } catch (err) { 108 | assert.equal(err.code, ErrorCodes.NODE_NOT_FOUND) 109 | } 110 | }) 111 | 112 | it('request-next-error', async () => { 113 | let expectedError = 'some error message' 114 | 115 | try { 116 | let expectedMessage = 'bar' 117 | 118 | await serverNode.bind() 119 | await clientNode.connect({ address: serverNode.getAddress() }) 120 | serverNode.onRequest('foo', ({ body, reply, next }) => { 121 | assert.equal(body, expectedMessage) 122 | next(expectedError) 123 | }) 124 | serverNode.onRequest('foo', ({ body, reply, next }) => { 125 | reply() 126 | }) 127 | 128 | await clientNode.request({ to: serverNode.getId(), event: 'foo', data: expectedMessage }) 129 | } catch (err) { 130 | assert.equal(err, expectedError) 131 | } 132 | }) 133 | 134 | it('request-error', async () => { 135 | let expectedError = 'some error message' 136 | 137 | try { 138 | let expectedMessage = 'bar' 139 | 140 | await serverNode.bind() 141 | await clientNode.connect({ address: serverNode.getAddress() }) 142 | serverNode.onRequest('foo', ({ body, reply, next, error }) => { 143 | assert.equal(body, expectedMessage) 144 | error(expectedError) 145 | }) 146 | 147 | await clientNode.request({ to: serverNode.getId(), event: 'foo', data: expectedMessage }) 148 | } catch (err) { 149 | assert.equal(err, expectedError) 150 | } 151 | }) 152 | 153 | it('tick after disconnect', async () => { 154 | try { 155 | await serverNode.bind() 156 | await clientNode.connect({ address: serverNode.getAddress() }) 157 | await clientNode.disconnect(serverNode.getAddress()) 158 | clientNode.tick({ to: serverNode.getId(), event: 'foo', data: 'bar' }) 159 | } catch (err) { 160 | assert.equal(err.code, ErrorCodes.NODE_NOT_FOUND) 161 | } 162 | }) 163 | 164 | it('request after unbind', async () => { 165 | try { 166 | await serverNode.bind() 167 | await clientNode.connect({ address: serverNode.getAddress() }) 168 | await serverNode.unbind() 169 | await serverNode.request({ to: clientNode.getId(), event: 'foo', data: 'bar' }) 170 | } catch (err) { 171 | assert.equal(err.message, `Sending failed as socket '${serverNode.getId()}' is not online`) 172 | } 173 | }) 174 | 175 | it('client failure', (done) => { 176 | let dealerClient = new Dealer() 177 | 178 | serverNode.on(NodeEvents.CLIENT_FAILURE, () => { 179 | done() 180 | }) 181 | 182 | serverNode.bind() 183 | .then(() => { 184 | return dealerClient.connect(serverNode.getAddress()) 185 | }) 186 | .then(() => { 187 | let requestData = { 188 | event: NodeEvents.CLIENT_CONNECTED, 189 | data: { 190 | actorId: dealerClient.getId(), 191 | options: {} 192 | }, 193 | mainEvent: true 194 | } 195 | 196 | return dealerClient.request(requestData) 197 | }) 198 | .then(() => { 199 | return dealerClient.close() 200 | }) 201 | }).timeout(15000) 202 | 203 | 204 | it('client ping', (done) => { 205 | let date = Date.now() 206 | 207 | serverNode.bind() 208 | .then(() => { 209 | return clientNode.connect({ address: serverNode.getAddress() }) 210 | }) 211 | .then(() => { 212 | setTimeout(() => { 213 | let client = serverNode.getClientInfo({ id: clientNode.getId() }) 214 | assert.isAbove(client.online, date) 215 | done() 216 | }, 10000) 217 | }) 218 | }).timeout(15000) 219 | 220 | 221 | it('server failure', (done) => { 222 | let routerServer = new Router() 223 | 224 | routerServer.onRequest(NodeEvents.CLIENT_CONNECTED, ({ reply }) => { 225 | let replyData = { 226 | actorId: routerServer.getId(), 227 | options: {} 228 | } 229 | 230 | reply(replyData) 231 | }, true) 232 | 233 | clientNode.on(NodeEvents.SERVER_FAILURE, () => { 234 | done() 235 | }) 236 | 237 | routerServer.bind('tcp://127.0.0.1:3000') 238 | .then(() => { 239 | return clientNode.connect({ address: routerServer.getAddress() }) 240 | }) 241 | .then(() => { 242 | return routerServer.close() 243 | }) 244 | }) 245 | }) 246 | 247 | describe('oneToOne successfully connected', () => { 248 | let clientNode, serverNode 249 | 250 | beforeEach(async () => { 251 | clientNode = new Node({}) 252 | serverNode = new Node({ bind: 'tcp://127.0.0.1:3000' }) 253 | await serverNode.bind() 254 | await clientNode.connect({ address: serverNode.getAddress() }) 255 | }) 256 | 257 | afterEach(async () => { 258 | await clientNode.stop() 259 | await serverNode.stop() 260 | clientNode = null 261 | serverNode = null 262 | }) 263 | 264 | it('tickFromClient', (done) => { 265 | let expectedMessage = 'bar' 266 | 267 | serverNode.onTick('foo', (message) => { 268 | assert.equal(message, expectedMessage) 269 | serverNode.offTick('foo') 270 | done() 271 | }) 272 | 273 | clientNode.tick({ to: serverNode.getId(), event: 'foo', data: expectedMessage }) 274 | }) 275 | 276 | it('tickFromServer', (done) => { 277 | let expectedMessage = 'bar' 278 | 279 | clientNode.onTick('foo', (message) => { 280 | assert.equal(message, expectedMessage) 281 | done() 282 | }) 283 | 284 | serverNode.tick({ to: clientNode.getId(), event: 'foo', data: expectedMessage }) 285 | }) 286 | 287 | it('requestFromClient', async () => { 288 | let expectedMessage = 'bar' 289 | let expectedMessage2 = 'baz' 290 | 291 | serverNode.onRequest('foo', ({ body, reply }) => { 292 | assert.equal(body, expectedMessage) 293 | reply(expectedMessage2) 294 | }) 295 | 296 | let response = await clientNode.request({ to: serverNode.getId(), event: 'foo', data: expectedMessage }) 297 | 298 | assert.equal(expectedMessage2, response) 299 | }) 300 | 301 | it('request-next', async () => { 302 | let expectedMessage = 'bar' 303 | let expectedMessage2 = 'baz' 304 | 305 | serverNode.onRequest('foo', ({ body, reply, next }) => { 306 | assert.equal(body, expectedMessage) 307 | next() 308 | }) 309 | serverNode.onRequest(/fo+/, ({ body, reply }) => { 310 | assert.equal(body, expectedMessage) 311 | reply(expectedMessage2) 312 | }) 313 | 314 | let response = await clientNode.request({ to: serverNode.getId(), event: 'foo', data: expectedMessage }) 315 | 316 | assert.equal(expectedMessage2, response) 317 | }) 318 | 319 | it('requestFromServer', async () => { 320 | let expectedMessage = 'bar' 321 | let expectedMessage2 = 'baz' 322 | 323 | clientNode.onRequest('foo', ({ body, reply }) => { 324 | assert.equal(body, expectedMessage) 325 | reply(expectedMessage2) 326 | }) 327 | 328 | let response = await serverNode.request({ to: clientNode.getId(), event: 'foo', data: expectedMessage }) 329 | 330 | assert.equal(expectedMessage2, response) 331 | }) 332 | 333 | it('set new options', async () => { 334 | await clientNode.setOptions(Object.assign({}, clientNode.getOptions(), { foo: 'bar' })) 335 | await serverNode.setOptions(Object.assign({}, serverNode.getOptions(), { foo: 'bar' })) 336 | assert.equal(serverNode.getOptions().foo, 'bar') 337 | assert.equal(clientNode.getOptions().foo, 'bar') 338 | }) 339 | 340 | it('set options event in server node', (done) => { 341 | serverNode.on(NodeEvents.OPTIONS_SYNC, ({ id, newOptions }) => { 342 | assert.deepEqual(clientNode.getOptions(), newOptions) 343 | assert.equal(id, clientNode.getId()) 344 | done() 345 | }) 346 | clientNode.setOptions(Object.assign({}, clientNode.getOptions(), { foo: 'bar' })) 347 | }) 348 | 349 | it('set options event in client node', (done) => { 350 | clientNode.on(NodeEvents.OPTIONS_SYNC, ({ id, newOptions }) => { 351 | assert.deepEqual(serverNode.getOptions(), newOptions) 352 | assert.equal(id, serverNode.getId()) 353 | done() 354 | }) 355 | serverNode.setOptions(Object.assign({}, serverNode.getOptions(), { foo: 'bar' })) 356 | }) 357 | }) 358 | 359 | describe('reconnect', () => { 360 | let clientNode, serverNode 361 | 362 | beforeEach(async () => { 363 | clientNode = new Node({}) 364 | serverNode = new Node({ bind: 'tcp://127.0.0.1:3000' }) 365 | await serverNode.bind() 366 | await clientNode.connect({ address: serverNode.getAddress(), reconnectionTimeout: 500 }) 367 | }) 368 | 369 | afterEach(async () => { 370 | await clientNode.stop() 371 | await serverNode.stop() 372 | clientNode = null 373 | serverNode = null 374 | }) 375 | 376 | it('reconnect failure', (done) => { 377 | clientNode.on(NodeEvents.SERVER_RECONNECT_FAILURE, () => { 378 | done() 379 | }) 380 | 381 | serverNode.unbind() 382 | }) 383 | 384 | it('successfully reconnect', (done) => { 385 | clientNode.on(NodeEvents.SERVER_RECONNECT, () => { 386 | serverNode.unbind().then(done) 387 | }) 388 | serverNode.unbind() 389 | .then(() => { 390 | serverNode.bind() 391 | }) 392 | }) 393 | }) 394 | 395 | describe('information', () => { 396 | let clientNode, serverNode 397 | 398 | beforeEach(async () => { 399 | clientNode = new Node({}) 400 | serverNode = new Node({ bind: 'tcp://127.0.0.1:3000' }) 401 | await serverNode.bind() 402 | await clientNode.connect({ address: serverNode.getAddress() }) 403 | }) 404 | 405 | afterEach(async () => { 406 | await clientNode.stop() 407 | await serverNode.stop() 408 | clientNode = null 409 | serverNode = null 410 | }) 411 | 412 | it('get server information with address', (done) => { 413 | let serverInfo = clientNode.getServerInfo({ address: serverNode.getAddress() }) 414 | assert.equal(serverInfo.id, serverNode.getId()) 415 | assert.equal(serverInfo.address, serverNode.getAddress()) 416 | assert.notEqual(serverInfo.online, false) 417 | done() 418 | }) 419 | 420 | it('get server information with id', (done) => { 421 | let serverInfo = clientNode.getServerInfo({ id: serverNode.getId() }) 422 | assert.equal(serverInfo.id, serverNode.getId()) 423 | assert.equal(serverInfo.address, serverNode.getAddress()) 424 | assert.notEqual(serverInfo.online, false) 425 | done() 426 | }) 427 | 428 | it('get server information, wrong id/address', (done) => { 429 | let serverInfo = clientNode.getServerInfo({ id: 'foo' }) 430 | assert.equal(serverInfo, null) 431 | done() 432 | }) 433 | 434 | it('get client information with id', (done) => { 435 | let clientInfo = serverNode.getClientInfo({ id: clientNode.getId() }) 436 | assert.equal(clientInfo.id, clientNode.getId()) 437 | assert.notEqual(clientInfo.online, false) 438 | done() 439 | }) 440 | 441 | it('get client information, wrong id', (done) => { 442 | let clientInfo = serverNode.getClientInfo({ id: 'foo' }) 443 | assert.equal(clientInfo, null) 444 | done() 445 | }) 446 | }) 447 | --------------------------------------------------------------------------------