├── .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 | 
2 |
3 |
4 | [](https://github.com/standard/standard)
5 | [](https://nodei.co/npm/zeronode/)
6 |
7 | [
](https://gitter.im/npm-zeronode/Lobby)
8 | [](https://snyk.io/test/github/sfast/zeronode)
9 | [](https://github.com/sfast/zeronode/blob/master/LICENSE)
10 | [](https://github.com/sfast/zeronode/issues)
11 |
12 | [](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 | [](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 | | Zeronode | Seneca (tcp) | Pigato |
97 | 1000 msg, 1kb data | 394ms | 2054ms | 342ms |
98 | 50000 msg, 1kb data | 11821ms | 140934ms | FAIL(100s timeout) |
99 |
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 | | is_internal |
11 | event_type |
12 | id_length |
13 | id |
14 | sender_length |
15 | sender id |
16 | receiver_length |
17 | receiver id |
18 | event_length |
19 | event |
20 | data |
21 |
22 |
23 | Type |
24 | bool |
25 | int8 |
26 | int8 |
27 | hex |
28 | int8 |
29 | utf-8 |
30 | int8 |
31 | utf-8 |
32 | int8 |
33 | utf-8 |
34 | JSON |
35 |
36 |
37 | length (bytes) |
38 | 1 | 1 |
39 | 1 |
40 | id_length |
41 | 1 |
42 | sender_length |
43 | 1 |
44 | receiver_length |
45 | 1 |
46 | event_length |
47 | remaining |
48 |
49 |
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 |
--------------------------------------------------------------------------------