├── .gitignore ├── README.md ├── demo ├── core-producer.js ├── core.js ├── media-producer.js ├── socket-producer.js └── socket.js ├── package-lock.json ├── package.json ├── src ├── amqpkit.js ├── index.js ├── lib │ ├── errors │ │ ├── clienterror.js │ │ ├── error-utils.js │ │ ├── extendableerror.js │ │ ├── index.js │ │ └── internalerror.js │ ├── event-emitter-extra │ │ ├── index.js │ │ └── listener.js │ ├── exchange.js │ ├── message.js │ ├── queue.js │ ├── response.js │ ├── router.js │ └── rpc.js ├── microservicekit.js └── shutdownkit.js └── test ├── amqpkit-tests.js ├── exchange-tests.js ├── lib ├── channel-stubs.js ├── connection-stubs.js ├── mocks │ └── message.js └── rpc-stubs.js ├── message-tests.js ├── queue-tests.js ├── response-tests.js ├── router-tests.js └── rpc-tests.js /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # microservice-kit 2 | 3 | Utility belt for building microservices. 4 | 5 | ## Quick Start 6 | 7 | - Check out [/demo](https://github.com/signalive/microservice-kit/tree/master/demo) folder 8 | - [A boilerplate for your new microservice](https://github.com/signalive/microservice-boilerplate) 9 | 10 | # API Reference 11 | 12 | ## Class MicroserviceKit 13 | 14 | This is the main class, the entry point to microservice-kit. To use it, you just need to import microservice-kit: 15 | 16 | ```javascript 17 | const MicroserviceKit = require('microservice-kit'); 18 | ``` 19 | 20 | To create an instance, look at constructor below. A microservice-kit instance is simply collection of an AmqpKit and a ShutdownKit instances. 21 | 22 | #### `new MicroserviceKit(options={})` 23 | 24 | ##### Params 25 | 26 | Name|Type|Description 27 | ----|----|----------- 28 | options.type="microservice"|String|Type of the microservice. This name will be used as prefix in generating unique name. This is helpful when differentiating microservice instances. 29 | options.amqp|Object|This object will be pass to AmqpKit when creating instance. See AmqpKit's docs for detail. 30 | [options.shutdown.logger=null]|Function|This function will be passed into `ShutdownKit.setLogger` method. 31 | 32 | ##### Sample 33 | 34 | ```javascript 35 | const microserviceKit = new MicroserviceKit({ 36 | type: 'core-worker', 37 | amqp: { 38 | url: "amqp://localhost", 39 | queues: [ 40 | { 41 | name: "core", 42 | options: {durable: true} 43 | } 44 | ], 45 | exchanges: [] 46 | } 47 | }); 48 | ``` 49 | 50 | #### `MicroserviceKit.prototype.amqpKit` 51 | 52 | This amqpKit instance is automatically created for microservice. See AmqpKit for details. 53 | 54 | ```javascript 55 | const coreQueue = microserviceKit.amqpKit.getQueue('core'); 56 | ``` 57 | 58 | #### `MicroserviceKit.prototype.shutdownKit` 59 | 60 | This shutdownKit (singleton) instance is automatically created for microservice. See ShutdownKit for details. 61 | 62 | ``` 63 | microserviceKit.shutdownKit.addJob(someFunction); 64 | ``` 65 | 66 | #### `MicroserviceKit.prototype.init()` -> `Promise` 67 | 68 | Created instance is not ready yet, it will connect to rabbitmq. You should call this method when booting your app. 69 | 70 | ```javascript 71 | microserviceKit 72 | .init() 73 | .then(() => { 74 | console.log("Initalized microservicekit!"); 75 | }) 76 | .catch((err) => { 77 | console.log("Cannot initalize microservicekit!", err); 78 | }) 79 | ``` 80 | 81 | #### `MicroserviceKit.prototype.getName()` -> `String` 82 | 83 | This is the unique name of the created instance. It begins with microservice type and followed by random string. Ex: `socket-worker-54a98630` 84 | 85 | ## Class AmqpKit 86 | 87 | This is the AmqpKit class aims to help communication over RabbitMQ. Main features: 88 | - Get callbacks like natively instead of low-level RabbitMQ RPC topologies 89 | - Send & recieve events instead of messages. Events are just special message hierarchy. 90 | - Send & recieve payloads in native JSON format instead of buffers. 91 | - Progress support, a consumer can inform its progress to the producer. 92 | 93 | AmqpKit uses `amqplib` in barebones. Look at [its documentation](http://www.squaremobius.net/amqp.node/channel_api.html). We will refer this page a lot. 94 | 95 | ```javascript 96 | const AmqpKit = require('microservice-kit').AmqpKit; 97 | ``` 98 | 99 | You can reach AmqpKit class like above. However, if you create a MicroserviceKit instance you don't need to reach AmqpKit. An AmqpKit instance will be automatically created for you. 100 | 101 | #### `new AmqpKit([options={}])` 102 | 103 | **Only use this constructor for advanced usage!** An AmqpKit instance will be automatically created, if you use `new MicroserviceKit(options)` constructor. If so, `options.amqp` will be used while creating AmqpKit instance. 104 | 105 | ##### Params 106 | 107 | Param|Type|Description 108 | -----|----|----------- 109 | [options.url]|String|AMQP connection string. Ex: `amqp://localhost` 110 | [options.rpc=true]|Boolean|If you don't need to use callbacks for amqp communication, you can use `false`. If so, an extra rpc channel and queue will not be created. Default `true`. 111 | options.queues=[]|Array|This queues will be asserted in `init` flow. 112 | [options.queues[].name]|String|Name of queue on RabbitMQ. Optional. Do not pass any parameter if you want to create an exclusive queue. It will be generated automatically. 113 | options.queues[].key|String|This is key value for accessing reference. This will be used for `AmqpKit.prototype.getQueue`. 114 | options.queues[].options|Object|Options for the queue. See offical [amqplib assertQueue reference](http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertQueue). 115 | options.exchanges=[]|Array|This exchanges will be asserted in `init` flow. 116 | options.exchanges[].name|String|Name of exchange on RabbitMQ. 117 | options.exchanges[].key|String|This is key value for accessing reference. This will be used for `AmqpKit.prototype.getExchange`. 118 | options.exchanges[].type|String|`fanout`, `direct` or `topic` 119 | options.exchanges[].options|Object|Options for the exchange. See offical [amqplib assertExchange reference](http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertExchange). 120 | [options.logger=null]|Function|AmqpKit can log incoming and outgoing events. It also logs how much time spend on consuming events or getting callback. You can use simply `console.log.bind(console)`. 121 | 122 | 123 | ##### Sample 124 | 125 | ```javascript 126 | const amqpKit = new AmqpKit({ 127 | queues: [ 128 | { 129 | key: 'broadcast', 130 | options: {exclusive: true} 131 | }, 132 | { 133 | key: 'direct', 134 | options: {exclusive: true} 135 | } 136 | ], 137 | exchanges: [ 138 | { 139 | name: 'socket-broadcast', 140 | key: 'socket-broadcast', 141 | type: 'fanout', 142 | options: {} 143 | }, 144 | { 145 | name: 'socket-direct', 146 | key: 'socket-direct', 147 | type: 'direct', 148 | options: {} 149 | } 150 | ], 151 | logger: function() { 152 | var args = Array.prototype.slice.call(arguments); 153 | args.unshift('[amqpkit]'); 154 | console.log.apply(console, args); 155 | } 156 | }); 157 | ``` 158 | 159 | #### `AmqpKit.prototype.prefetch(count, [global])` 160 | 161 | AmqpKit has two channels by default. The common channel, is used for recieving and sending messages in your microservice. Another channel is for getting rpc callbacks and used exclusively inside AmqpKit. 162 | This method sets a limit the number of unacknowledged messages on the common channel. If this limit is reached, RabbitMQ won't send any events to microservice. 163 | 164 | ##### Params 165 | 166 | Param|Type|Description 167 | -----|----|----------- 168 | count|Number|Set the prefetch count for the channel. The count given is the maximum number of messages sent over the channel that can be awaiting acknowledgement; once there are count messages outstanding, the server will not send more messages on this channel until one or more have been acknowledged. A falsey value for count indicates no such limit. 169 | [global]|Boolean|Use the global flag to get the per-channel behaviour. Use `true` if you want to limit the whole microservice. RPC channel is seperate, so don't worry about callbacks. 170 | 171 | ##### Sample 172 | 173 | ```javascript 174 | microserviceKit.amqpKit.prefetch(100, true); 175 | ``` 176 | 177 | This microservice can process maximum 100 events at the same time. (Event type does not matter) RabbitMQ won't send any message to the microservice until it completes some jobs. 178 | 179 | #### `AmqpKit.prototype.getQueue(key)` -> `AmqpKit.Queue` 180 | 181 | Gets queue instance by key. 182 | 183 | Param|Type|Description 184 | -----|----|----------- 185 | key|String|Unique queue key. 186 | 187 | #### `AmqpKit.prototype.getExchange(key)` -> `AmqpKit.Exchange` 188 | 189 | Gets exchange instance by key. 190 | 191 | Param|Type|Description 192 | -----|----|----------- 193 | key|String|Unique exhange key. 194 | 195 | #### `AmqpKit.prototype.createQueue(key, name, options={})` -> `Promise.` 196 | 197 | Creates (assert) a queue. 198 | 199 | Param|Type|Description 200 | -----|----|----------- 201 | key|String|Unique queue key. 202 | [name]|String|Name of queue on RabbitMQ. Optional. Pass empty string if you want to create an exclusive queue. It will be generated automatically. 203 | options|Object|Options for the queue. See offical [amqplib assertQueue reference](http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertQueue). 204 | 205 | #### `AmqpKit.prototype.createExchange(key, name, type, options={})` -> `Promise.` 206 | 207 | Creates (asserts) an exchange. 208 | 209 | Param|Type|Description 210 | -----|----|----------- 211 | key|String|Unique exhange key. 212 | name|String|Name of exchange on RabbitMQ. 213 | type|String|`fanout`, `direct` or `topic` 214 | options|Object|Options for the exchange. See offical [amqplib assertExchange reference](http://www.squaremobius.net/amqp.node/channel_api.html#channel_assertExchange). 215 | 216 | #### `AmqpKit.prototype.connection` 217 | 218 | Native `ampqlib`s connection. See [offical docs](http://www.squaremobius.net/amqp.node/channel_api.html#connect). 219 | 220 | #### `AmqpKit.prototype.channel` 221 | 222 | Native `ampqlib`s channel instance that will be used commonly. See [offical docs](http://www.squaremobius.net/amqp.node/channel_api.html#channel). 223 | 224 | ## Class AmqpKit.Queue 225 | 226 | This class is not exposed to user. When you do `amqpKit.getQueue()` or `amqpKit.createQueue()`, what you get is an instance of this class. 227 | 228 | #### `AmqpKit.Queue.prototype.consumeEvent(eventName, callback, [options={}])` 229 | 230 | Sends an event to queue. 231 | 232 | ##### Params 233 | 234 | Param|Type|Description 235 | -----|----|----------- 236 | eventName|String|Event name. 237 | callback|Function|Handler function. It takes 3 parameters: `payload`, `done`, `progress`. Payload is event payload. Done is node style callback that finalize the event: `done(err, payload)`. Both error and payload is optional. Error should be instaceof native Error class! Progress is optional callback that you can send progress events: `progress(payload)`. Progress events does not finalize events! 238 | [options={}]|Object|Consume options. See `amqplib`s [offical consume docs](http://www.squaremobius.net/amqp.node/channel_api.html#channel_consume). 239 | 240 | ##### Sample 241 | 242 | ```javascript 243 | const coreQueue = microserviceKit.amqpKit.getQueue('core'); 244 | 245 | coreQueue.consumeEvent('get-device', (payload, done, progress) => { 246 | // Optional progress events! 247 | let count = 0; 248 | let interval = setInterval(() => { 249 | progress({data: 'Progress ' + (++count) + '/5'}); 250 | }, 1000); 251 | 252 | // complete job. 253 | setTimeout(() => { 254 | clearInterval(interval); 255 | done(null, {some: 'Response!'}); 256 | }, 5000); 257 | }, {}); 258 | ``` 259 | 260 | #### `AmqpKit.Queue.prototype.bind(exhange, pattern)` -> `Promise` 261 | 262 | Assert a routing pattern from an exchange to the queue: the exchange named by source will relay messages to the queue named, according to the type of the exchange and the pattern given. 263 | 264 | ##### Params 265 | 266 | Param|Type|Description 267 | -----|----|----------- 268 | exchange|String|Name of exchange on RabbitMQ. 269 | pattern|String|Binding pattern. 270 | 271 | #### `AmqpKit.Queue.prototype.unbind(exchange, pattern)` -> `Promise` 272 | 273 | Remove a routing path between the queue named and the exchange named as source with the pattern and arguments given. 274 | 275 | Param|Type|Description 276 | -----|----|----------- 277 | exchange|String|Name of exchange on RabbitMQ. 278 | pattern|String|Binding pattern. 279 | 280 | #### `AmqpKit.Queue.prototype.getUniqueName()` -> `String` 281 | 282 | Returns real queue name on RabbitMQ. 283 | 284 | #### `AmqpKit.Queue.prototype.sendEvent(eventName, [payload={}], [options={}])` -> `Promise` 285 | 286 | Sends an event with payload to the queue. 287 | 288 | ##### Params 289 | 290 | Param|Type|Description 291 | -----|----|----------- 292 | eventName|String|Event name. 293 | [payload]|Object|Payload data. 294 | [options]|Object|See `ampqlib`s [official docs](http://www.squaremobius.net/amqp.node/channel_api.html#channel_publish). 295 | [options.dontExpectRpc=false]|Boolean|Additional to `amqplib` options, we provide couple of functions too. If you don't want to callback for this message, set `true`. Default `false`. 296 | [options.timeout=30000]|Number|Timeout duration. This check is totaly in producer side, if job is done after timeout, it's rpc message will be ignored. Pass `0` if you dont want to timeout. If you set `dontExpectRpc` as `true`, ignore this option. 297 | 298 | ##### Sample 299 | 300 | ```javascript 301 | const coreQueue = microserviceKit.amqpKit.getQueue('core'); 302 | 303 | coreQueue 304 | .sendEvent('get-device', {id: 5}, {persistent: true}) 305 | .progress((payload) => { 306 | console.log('The job is processing...', payload); 307 | }) 308 | .success((payload) => { 309 | console.log('Device: ', payload); 310 | }) 311 | .catch((err) => { 312 | console.log('Cannot get device', err); 313 | }) 314 | ``` 315 | 316 | Notice the `.progress()` handler? It's just a additonal handler that AmqpKit puts for you. Instead of this, return value of this method is `Promise`. 317 | 318 | ## Class AmqpKit.Exchange 319 | 320 | This class is not exposed to user. When you do `amqpKit.getExchange()` or `amqpKit.createExchange()`, what you get is an instance of this class. 321 | 322 | #### `AmqpKit.Exchange.prototype.publishEvent(routingKey, eventName, [payload], [options])` -> `Promise` 323 | 324 | Sends an event with payload to the exchange. 325 | 326 | ##### Params 327 | 328 | Param|Type|Description 329 | -----|----|----------- 330 | routingKey|String|Routing pattern for event! 331 | eventName|String|Event name. 332 | [payload]|Object|Payload data. 333 | [options]|Object|See `ampqlib`s [official docs](http://www.squaremobius.net/amqp.node/channel_api.html#channel_publish). 334 | [options.dontExpectRpc=false]|Boolean|Additional to `amqplib` options, we provide couple of functions too. If you don't want to callback for this message, set `true`. Default `false`. 335 | [options.timeout=30000]|Number|Timeout duration. This check is totaly in producer side, if job is done after timeout, it's rpc message will be ignored. Pass `0` if you dont want to timeout. If you set `dontExpectRpc` as `true`, ignore this option. 336 | 337 | ##### Sample 338 | 339 | ```javascript 340 | const broadcastExchange = microserviceKit.amqpKit.getExchange('socket-broadcast'); 341 | broadcastExchange.publishEvent('', 'channel-updated', {channel: 'data'}, {dontExpectRpc: true}); 342 | ``` 343 | 344 | ## Class ShutdownKit 345 | 346 | This class helps us to catch interrupt signals, uncaught exceptions and tries to perform jobs to shutdown gracefully. This class is singleton. 347 | 348 | ```javascript 349 | // Direct access 350 | const shutdownKit = require('microservice-kit').ShutdownKit; 351 | 352 | // Or from microservice-kit instance 353 | const microserviceKit = new MicroserviceKit({...}); 354 | console.log(microserviceKit.shutdownKit); 355 | ``` 356 | 357 | As you can see above, you can access ShutdownKit singleton instance in multiple ways. 358 | 359 | 360 | #### `ShutdownKit.prototype.addJob(job)` 361 | 362 | Add a job to graceful shutdown process. When ShutdownKit tries to shutdown gracefully, it runs all the jobs in parallel. 363 | 364 | ##### Params 365 | 366 | Param|Type|Description 367 | -----|----|----------- 368 | job|Function|This function takes `done` callback as single parameter. Execute `done` callback when job is completed. It's also like node-style callback: `done(err)`. 369 | 370 | ##### Sample 371 | 372 | ```javascript 373 | shutdownKit.addJob((done) => { 374 | debug('Closing connection...'); 375 | this.connection 376 | .close() 377 | .then(() => { 378 | done(); 379 | }) 380 | .catch(done); 381 | }); 382 | ``` 383 | 384 | #### `ShutdownKit.prototype.gracefulShutdown()` 385 | 386 | This method gracefully shutdowns current node process. 387 | 388 | #### `ShutdownKit.prototype.setLogger(logger)` 389 | 390 | Sets a custom logger to print out shutdown process logs to console. 391 | 392 | ##### Params 393 | 394 | Param|Type|Description 395 | -----|----|----------- 396 | logger|Function|This function takes `done` callback as single parameter. Execute `done` callback when job is completed. It's also like node-style callback: `done(err)`. 397 | 398 | ##### Sample 399 | 400 | ```javascript 401 | shutdownKit.setLogger(() => { 402 | var args = Array.prototype.slice.call(arguments); 403 | args.unshift('[shutdownkit]'); 404 | console.log.apply(console, args); 405 | }); 406 | ``` 407 | 408 | As you can see, we convert all arguments to native array and prepends `[shutdown]` prefix. Then apply this arguments to standart console.log method. 409 | -------------------------------------------------------------------------------- /demo/core-producer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MicroserviceKit = require('../src'); 4 | const Errors = require('../src/lib/errors'); 5 | 6 | 7 | const microserviceKit = new MicroserviceKit({ 8 | type: 'some-core-producer-worker', 9 | config: null, // Dont use config file! 10 | amqp: { 11 | queues: [ 12 | { 13 | name: 'core', 14 | key: 'core', 15 | options: {durable: true} 16 | } 17 | ], 18 | logger: function() { 19 | var args = Array.prototype.slice.call(arguments); 20 | args.unshift('[amqpkit]'); 21 | console.log.apply(console, args); 22 | } 23 | } 24 | }); 25 | 26 | microserviceKit 27 | .init() 28 | .then(() => { 29 | const coreQueue = microserviceKit.amqpKit.getQueue('core'); 30 | 31 | coreQueue 32 | .sendEvent('deneme.job', {some: 'data!'}, {persistent: true}) 33 | .progress((data) => { 34 | console.log('Progressing...' + JSON.stringify(data)); 35 | }) 36 | .then((response) => { 37 | console.log('Positive response: ' + JSON.stringify(response)); 38 | }) 39 | .catch((err) => { 40 | console.log('Negative response: ', err); 41 | }); 42 | }) 43 | .catch((err) => { 44 | console.log('Cannot boot'); 45 | console.log(err.stack); 46 | }); 47 | -------------------------------------------------------------------------------- /demo/core.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MicroserviceKit = require('../src'); 4 | const Errors = require('../src/lib/errors'); 5 | 6 | 7 | const microserviceKit = new MicroserviceKit({ 8 | type: 'core-worker', 9 | config: null, // Dont use config file! 10 | amqp: { 11 | queues: [ 12 | { 13 | name: 'core', 14 | key: 'core', 15 | options: {durable: true} 16 | } 17 | ], 18 | logger: function() { 19 | var args = Array.prototype.slice.call(arguments); 20 | args.unshift('[amqpkit]'); 21 | console.log.apply(console, args); 22 | } 23 | }, 24 | shutdown: { 25 | logger: function() { 26 | var args = Array.prototype.slice.call(arguments); 27 | args.unshift('[shutdownkit]'); 28 | console.log.apply(console, args); 29 | } 30 | } 31 | }); 32 | 33 | 34 | microserviceKit 35 | .init() 36 | .then(() => { 37 | // Run phase 38 | console.log("Waiting for messages in %s. To exit press CTRL+C", 'core'); 39 | 40 | const coreQueue = microserviceKit.amqpKit.getQueue('core'); 41 | 42 | // Consume some core jobs! 43 | coreQueue.consumeEvent('deneme.job', (data, callback, progress, routingKey) => { 44 | console.log("Received: " + JSON.stringify(data)); 45 | console.log("The routing key of the job was", routingKey); 46 | 47 | // Dummy progress events 48 | let count = 0; 49 | let interval = setInterval(() => { 50 | progress({data: 'Progress ' + (++count) + '/5'}); 51 | }, 1000); 52 | 53 | // Dummy complete job. 54 | setTimeout(() => { 55 | clearInterval(interval); 56 | callback(new Errors.ClientError('Anaynin amugg')); 57 | //callback(null, {some: 'Responseee!'}); 58 | console.log('Done.'); 59 | }, 5000); 60 | }); 61 | }) 62 | .catch((err) => { 63 | console.log('Cannot boot'); 64 | console.log(err.stack); 65 | }); 66 | -------------------------------------------------------------------------------- /demo/media-producer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MicroserviceKit = require('../src'); 4 | const Errors = require('../src/lib/errors'); 5 | 6 | 7 | const microserviceKit = new MicroserviceKit({ 8 | type: 'some-media-producer-worker', 9 | config: null, // Dont use config file! 10 | amqp: { 11 | queues: [ 12 | { 13 | name: 'media', 14 | key: 'media', 15 | options: {durable: true} 16 | } 17 | ], 18 | logger: function() { 19 | var args = Array.prototype.slice.call(arguments); 20 | args.unshift('[amqpkit]'); 21 | console.log.apply(console, args); 22 | } 23 | } 24 | }); 25 | 26 | microserviceKit 27 | .init() 28 | .then(() => { 29 | const mediaQueue = microserviceKit.amqpKit.getQueue('media'); 30 | 31 | mediaQueue 32 | .sendEvent('media-process', { 33 | id: '123456', 34 | mediaUrl: '' 35 | }, {persistent: true}) 36 | .progress((data) => { 37 | console.log('Progressing...' + JSON.stringify(data)); 38 | }) 39 | .then((response) => { 40 | console.log('Positive response: ' + JSON.stringify(response)); 41 | }) 42 | .catch((err) => { 43 | console.log('Negative response: ', err); 44 | }); 45 | }) 46 | .catch((err) => { 47 | console.log('Cannot boot'); 48 | console.log(err.stack); 49 | }); 50 | -------------------------------------------------------------------------------- /demo/socket-producer.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MicroserviceKit = require('../src'); 4 | 5 | 6 | const microserviceKit = new MicroserviceKit({ 7 | type: 'socket-producer', 8 | config: null, // Dont use config file! 9 | amqp: { 10 | exchanges: [ 11 | { 12 | name: 'socket-broadcast', 13 | key: 'socket-broadcast', 14 | type: 'fanout', 15 | options: {} 16 | }, 17 | { 18 | name: 'socket-direct', 19 | key: 'socket-direct', 20 | type: 'direct', 21 | options: {} 22 | } 23 | ], 24 | logger: function() { 25 | var args = Array.prototype.slice.call(arguments); 26 | args.unshift('[amqpkit]'); 27 | console.log.apply(console, args); 28 | } 29 | } 30 | }); 31 | 32 | microserviceKit 33 | .init() 34 | .then(() => { 35 | // Run phase 36 | // Broadcast 37 | 38 | const broadcastExchange = microserviceKit.amqpKit.getExchange('socket-broadcast'); 39 | const directExchange = microserviceKit.amqpKit.getExchange('socket-direct'); 40 | 41 | broadcastExchange 42 | .publishEvent( 43 | '', 44 | 'signa.socket.broadcast.update-channel', 45 | {txt: 'channel update detail here.'}, 46 | {dontExpectRpc: true} 47 | ) 48 | .then((response) => { 49 | console.log('Sent pubsub message.'); 50 | }) 51 | .catch((err) => { 52 | console.log('Cannot send pubsub message.'); 53 | }); 54 | 55 | broadcastExchange 56 | .publishEvent( 57 | '', 58 | 'signa.socket.broadcast.new-app-version', 59 | {txt: 'new app version falan.'}, 60 | {dontExpectRpc: true} 61 | ) 62 | .then((response) => { 63 | console.log('Sent pubsub message.'); 64 | }) 65 | .catch((err) => { 66 | console.log('Cannot send pubsub message.'); 67 | }); 68 | 69 | // Direct 70 | directExchange 71 | .publishEvent( 72 | 'device-uuid', 73 | 'signa.socket.direct.update-device', 74 | {txt:'Update device falan.'} 75 | ) 76 | .then((response) => { 77 | console.log('Positive response: ' + JSON.stringify(response)); 78 | }) 79 | .catch((err) => { 80 | console.log('Negative response: ' + err); 81 | }); 82 | 83 | directExchange 84 | .publishEvent( 85 | 'device-uuid', 86 | 'signa.socket.direct.screenshot', 87 | {txt:'Screenshot request kanka.'} 88 | ) 89 | .then((response) => { 90 | console.log('Positive response: ' + JSON.stringify(response)); 91 | }) 92 | .catch((err) => { 93 | console.log('Negative response: ' + err); 94 | }); 95 | 96 | }) 97 | .catch((err) => { 98 | console.log('Cannot boot'); 99 | console.log(err); 100 | }); 101 | -------------------------------------------------------------------------------- /demo/socket.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const MicroserviceKit = require('../src'); 4 | 5 | 6 | const microserviceKit = new MicroserviceKit({ 7 | type: 'socket-worker', 8 | config: null, // Dont use config file! 9 | amqp: { 10 | queues: [ 11 | { 12 | key: 'broadcast', 13 | options: {exclusive: true} 14 | }, 15 | { 16 | key: 'direct', 17 | options: {exclusive: true} 18 | } 19 | ], 20 | exchanges: [ 21 | { 22 | name: 'socket-broadcast', 23 | key: 'socket-broadcast', 24 | type: 'fanout', 25 | options: {} 26 | }, 27 | { 28 | name: 'socket-direct', 29 | key: 'socket-direct', 30 | type: 'direct', 31 | options: {} 32 | } 33 | ], 34 | logger: function() { 35 | var args = Array.prototype.slice.call(arguments); 36 | args.unshift('[amqpkit]'); 37 | console.log.apply(console, args); 38 | } 39 | } 40 | }); 41 | 42 | microserviceKit 43 | .init() 44 | .then(() => { 45 | console.log("Waiting for messages in %s and %s. To exit press CTRL+C", 'broadcast', 'direct'); 46 | 47 | const broadcastQueue = microserviceKit.amqpKit.getQueue('broadcast'); 48 | const directQueue = microserviceKit.amqpKit.getQueue('direct'); 49 | 50 | // Bind to broadcast exchange 51 | broadcastQueue.bind('socket-broadcast', ''); 52 | 53 | /** 54 | * On device connect 55 | */ 56 | function onDeviceConnect(device) { 57 | directQueue.bind('socket-direct', device.uuid); 58 | } 59 | 60 | /** 61 | * On device disconnect 62 | */ 63 | function onDeviceDisconnect(device) { 64 | directQueue.unbind('socket-direct', device.uuid); 65 | } 66 | 67 | if (Math.random() >= 0.5) { 68 | console.log('Connected device: `device-uuid`'); 69 | var device = {uuid: 'device-uuid'}; 70 | onDeviceConnect(device); 71 | } 72 | 73 | 74 | 75 | /** 76 | * Consume socket jobs! 77 | */ 78 | broadcastQueue.consumeEvent('signa.socket.broadcast.update-channel', (data) => { 79 | console.log("Received channel update: " + JSON.stringify(data)); 80 | }, {noAck: true}); 81 | 82 | broadcastQueue.consumeEvent('signa.socket.broadcast.new-app-version', (data) => { 83 | console.log("Received new app version: " + JSON.stringify(data)); 84 | }, {noAck: true}); 85 | 86 | directQueue.consumeEvent('signa.socket.direct.update-device', (data, callback, progress, routingKey) => { 87 | console.log("Received update device: " + JSON.stringify(data)); 88 | console.log("The routing key of the job was", routingKey); 89 | 90 | callback(null, {some: 'device updated kanka, no worries.'}); 91 | }); 92 | 93 | directQueue.consumeEvent('signa.socket.direct.screenshot', (data, callback, progress, routingKey) => { 94 | console.log("Received update device: " + JSON.stringify(data)); 95 | console.log("The routing key of the job was", routingKey); 96 | 97 | setTimeout(() => { 98 | let rand = Math.random(); 99 | callback(null, {some: 'screenshot ' + rand}); 100 | console.log("Done screenshot.request " + rand); 101 | }, 5000); 102 | }); 103 | }) 104 | .catch((err) => { 105 | console.log('Cannot boot'); 106 | console.log(err); 107 | }); 108 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microservice-kit", 3 | "version": "0.9.1", 4 | "lockfileVersion": 3, 5 | "requires": true, 6 | "packages": { 7 | "": { 8 | "name": "microservice-kit", 9 | "version": "0.9.1", 10 | "license": "ISC", 11 | "dependencies": { 12 | "amqplib": "0.10.8", 13 | "async": "2.6.1", 14 | "async-q": "0.3.1", 15 | "chance": "1.0.10", 16 | "debug": "4.4.1", 17 | "lodash": "4.17.21", 18 | "uuid": "11.1.0" 19 | }, 20 | "devDependencies": { 21 | "chai": "5.2.0", 22 | "chai-as-promised": "8.0.1", 23 | "mocha": "11.5.0", 24 | "sinon": "20.0.0", 25 | "sinon-chai": "4.0.0" 26 | } 27 | }, 28 | "node_modules/@isaacs/cliui": { 29 | "version": "8.0.2", 30 | "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", 31 | "integrity": "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==", 32 | "dev": true, 33 | "license": "ISC", 34 | "dependencies": { 35 | "string-width": "^5.1.2", 36 | "string-width-cjs": "npm:string-width@^4.2.0", 37 | "strip-ansi": "^7.0.1", 38 | "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", 39 | "wrap-ansi": "^8.1.0", 40 | "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" 41 | }, 42 | "engines": { 43 | "node": ">=12" 44 | } 45 | }, 46 | "node_modules/@pkgjs/parseargs": { 47 | "version": "0.11.0", 48 | "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", 49 | "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", 50 | "dev": true, 51 | "license": "MIT", 52 | "optional": true, 53 | "engines": { 54 | "node": ">=14" 55 | } 56 | }, 57 | "node_modules/@sinonjs/commons": { 58 | "version": "3.0.1", 59 | "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", 60 | "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", 61 | "dev": true, 62 | "license": "BSD-3-Clause", 63 | "dependencies": { 64 | "type-detect": "4.0.8" 65 | } 66 | }, 67 | "node_modules/@sinonjs/fake-timers": { 68 | "version": "13.0.5", 69 | "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-13.0.5.tgz", 70 | "integrity": "sha512-36/hTbH2uaWuGVERyC6da9YwGWnzUZXuPro/F2LfsdOsLnCojz/iSH8MxUt/FD2S5XBSVPhmArFUXcpCQ2Hkiw==", 71 | "dev": true, 72 | "license": "BSD-3-Clause", 73 | "dependencies": { 74 | "@sinonjs/commons": "^3.0.1" 75 | } 76 | }, 77 | "node_modules/@sinonjs/samsam": { 78 | "version": "8.0.2", 79 | "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.2.tgz", 80 | "integrity": "sha512-v46t/fwnhejRSFTGqbpn9u+LQ9xJDse10gNnPgAcxgdoCDMXj/G2asWAC/8Qs+BAZDicX+MNZouXT1A7c83kVw==", 81 | "dev": true, 82 | "license": "BSD-3-Clause", 83 | "dependencies": { 84 | "@sinonjs/commons": "^3.0.1", 85 | "lodash.get": "^4.4.2", 86 | "type-detect": "^4.1.0" 87 | } 88 | }, 89 | "node_modules/@sinonjs/samsam/node_modules/type-detect": { 90 | "version": "4.1.0", 91 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.1.0.tgz", 92 | "integrity": "sha512-Acylog8/luQ8L7il+geoSxhEkazvkslg7PSNKOX59mbB9cOveP5aq9h74Y7YU8yDpJwetzQQrfIwtf4Wp4LKcw==", 93 | "dev": true, 94 | "license": "MIT", 95 | "engines": { 96 | "node": ">=4" 97 | } 98 | }, 99 | "node_modules/amqplib": { 100 | "version": "0.10.8", 101 | "resolved": "https://registry.npmjs.org/amqplib/-/amqplib-0.10.8.tgz", 102 | "integrity": "sha512-Tfn1O9sFgAP8DqeMEpt2IacsVTENBpblB3SqLdn0jK2AeX8iyCvbptBc8lyATT9bQ31MsjVwUSQ1g8f4jHOUfw==", 103 | "license": "MIT", 104 | "dependencies": { 105 | "buffer-more-ints": "~1.0.0", 106 | "url-parse": "~1.5.10" 107 | }, 108 | "engines": { 109 | "node": ">=10" 110 | } 111 | }, 112 | "node_modules/ansi-regex": { 113 | "version": "6.1.0", 114 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", 115 | "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", 116 | "dev": true, 117 | "license": "MIT", 118 | "engines": { 119 | "node": ">=12" 120 | }, 121 | "funding": { 122 | "url": "https://github.com/chalk/ansi-regex?sponsor=1" 123 | } 124 | }, 125 | "node_modules/ansi-styles": { 126 | "version": "4.3.0", 127 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", 128 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", 129 | "dev": true, 130 | "license": "MIT", 131 | "dependencies": { 132 | "color-convert": "^2.0.1" 133 | }, 134 | "engines": { 135 | "node": ">=8" 136 | }, 137 | "funding": { 138 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 139 | } 140 | }, 141 | "node_modules/argparse": { 142 | "version": "2.0.1", 143 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", 144 | "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", 145 | "dev": true, 146 | "license": "Python-2.0" 147 | }, 148 | "node_modules/assertion-error": { 149 | "version": "2.0.1", 150 | "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", 151 | "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", 152 | "dev": true, 153 | "license": "MIT", 154 | "engines": { 155 | "node": ">=12" 156 | } 157 | }, 158 | "node_modules/async": { 159 | "version": "2.6.1", 160 | "resolved": "https://registry.npmjs.org/async/-/async-2.6.1.tgz", 161 | "integrity": "sha512-fNEiL2+AZt6AlAw/29Cr0UDe4sRAHCpEHh54WMz+Bb7QfNcFw4h3loofyJpLeQs4Yx7yuqu/2dLgM5hKOs6HlQ==", 162 | "dependencies": { 163 | "lodash": "^4.17.10" 164 | } 165 | }, 166 | "node_modules/async-q": { 167 | "version": "0.3.1", 168 | "resolved": "https://registry.npmjs.org/async-q/-/async-q-0.3.1.tgz", 169 | "integrity": "sha1-WxZwlTYbMm0m634rJaBSvTOaV8Q=", 170 | "dependencies": { 171 | "q": "~1.4.1", 172 | "throat": "~1.0.0" 173 | } 174 | }, 175 | "node_modules/balanced-match": { 176 | "version": "1.0.2", 177 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", 178 | "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", 179 | "dev": true, 180 | "license": "MIT" 181 | }, 182 | "node_modules/brace-expansion": { 183 | "version": "2.0.1", 184 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", 185 | "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", 186 | "dev": true, 187 | "license": "MIT", 188 | "dependencies": { 189 | "balanced-match": "^1.0.0" 190 | } 191 | }, 192 | "node_modules/browser-stdout": { 193 | "version": "1.3.1", 194 | "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", 195 | "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", 196 | "dev": true, 197 | "license": "ISC" 198 | }, 199 | "node_modules/buffer-more-ints": { 200 | "version": "1.0.0", 201 | "resolved": "https://registry.npmjs.org/buffer-more-ints/-/buffer-more-ints-1.0.0.tgz", 202 | "integrity": "sha512-EMetuGFz5SLsT0QTnXzINh4Ksr+oo4i+UGTXEshiGCQWnsgSs7ZhJ8fzlwQ+OzEMs0MpDAMr1hxnblp5a4vcHg==", 203 | "license": "MIT" 204 | }, 205 | "node_modules/camelcase": { 206 | "version": "6.3.0", 207 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", 208 | "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", 209 | "dev": true, 210 | "license": "MIT", 211 | "engines": { 212 | "node": ">=10" 213 | }, 214 | "funding": { 215 | "url": "https://github.com/sponsors/sindresorhus" 216 | } 217 | }, 218 | "node_modules/chai": { 219 | "version": "5.2.0", 220 | "resolved": "https://registry.npmjs.org/chai/-/chai-5.2.0.tgz", 221 | "integrity": "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw==", 222 | "dev": true, 223 | "license": "MIT", 224 | "dependencies": { 225 | "assertion-error": "^2.0.1", 226 | "check-error": "^2.1.1", 227 | "deep-eql": "^5.0.1", 228 | "loupe": "^3.1.0", 229 | "pathval": "^2.0.0" 230 | }, 231 | "engines": { 232 | "node": ">=12" 233 | } 234 | }, 235 | "node_modules/chai-as-promised": { 236 | "version": "8.0.1", 237 | "resolved": "https://registry.npmjs.org/chai-as-promised/-/chai-as-promised-8.0.1.tgz", 238 | "integrity": "sha512-OIEJtOL8xxJSH8JJWbIoRjybbzR52iFuDHuF8eb+nTPD6tgXLjRqsgnUGqQfFODxYvq5QdirT0pN9dZ0+Gz6rA==", 239 | "dev": true, 240 | "license": "MIT", 241 | "dependencies": { 242 | "check-error": "^2.0.0" 243 | }, 244 | "peerDependencies": { 245 | "chai": ">= 2.1.2 < 6" 246 | } 247 | }, 248 | "node_modules/chalk": { 249 | "version": "4.1.2", 250 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", 251 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", 252 | "dev": true, 253 | "license": "MIT", 254 | "dependencies": { 255 | "ansi-styles": "^4.1.0", 256 | "supports-color": "^7.1.0" 257 | }, 258 | "engines": { 259 | "node": ">=10" 260 | }, 261 | "funding": { 262 | "url": "https://github.com/chalk/chalk?sponsor=1" 263 | } 264 | }, 265 | "node_modules/chalk/node_modules/supports-color": { 266 | "version": "7.2.0", 267 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 268 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 269 | "dev": true, 270 | "license": "MIT", 271 | "dependencies": { 272 | "has-flag": "^4.0.0" 273 | }, 274 | "engines": { 275 | "node": ">=8" 276 | } 277 | }, 278 | "node_modules/chance": { 279 | "version": "1.0.10", 280 | "resolved": "https://registry.npmjs.org/chance/-/chance-1.0.10.tgz", 281 | "integrity": "sha1-A1ALBK2U53jdKJGwnsc6ath7GZY=" 282 | }, 283 | "node_modules/check-error": { 284 | "version": "2.1.1", 285 | "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.1.tgz", 286 | "integrity": "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw==", 287 | "dev": true, 288 | "license": "MIT", 289 | "engines": { 290 | "node": ">= 16" 291 | } 292 | }, 293 | "node_modules/chokidar": { 294 | "version": "4.0.3", 295 | "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", 296 | "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", 297 | "dev": true, 298 | "license": "MIT", 299 | "dependencies": { 300 | "readdirp": "^4.0.1" 301 | }, 302 | "engines": { 303 | "node": ">= 14.16.0" 304 | }, 305 | "funding": { 306 | "url": "https://paulmillr.com/funding/" 307 | } 308 | }, 309 | "node_modules/cliui": { 310 | "version": "8.0.1", 311 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", 312 | "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", 313 | "dev": true, 314 | "license": "ISC", 315 | "dependencies": { 316 | "string-width": "^4.2.0", 317 | "strip-ansi": "^6.0.1", 318 | "wrap-ansi": "^7.0.0" 319 | }, 320 | "engines": { 321 | "node": ">=12" 322 | } 323 | }, 324 | "node_modules/cliui/node_modules/ansi-regex": { 325 | "version": "5.0.1", 326 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 327 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 328 | "dev": true, 329 | "license": "MIT", 330 | "engines": { 331 | "node": ">=8" 332 | } 333 | }, 334 | "node_modules/cliui/node_modules/emoji-regex": { 335 | "version": "8.0.0", 336 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 337 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 338 | "dev": true, 339 | "license": "MIT" 340 | }, 341 | "node_modules/cliui/node_modules/string-width": { 342 | "version": "4.2.3", 343 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 344 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 345 | "dev": true, 346 | "license": "MIT", 347 | "dependencies": { 348 | "emoji-regex": "^8.0.0", 349 | "is-fullwidth-code-point": "^3.0.0", 350 | "strip-ansi": "^6.0.1" 351 | }, 352 | "engines": { 353 | "node": ">=8" 354 | } 355 | }, 356 | "node_modules/cliui/node_modules/strip-ansi": { 357 | "version": "6.0.1", 358 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 359 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 360 | "dev": true, 361 | "license": "MIT", 362 | "dependencies": { 363 | "ansi-regex": "^5.0.1" 364 | }, 365 | "engines": { 366 | "node": ">=8" 367 | } 368 | }, 369 | "node_modules/cliui/node_modules/wrap-ansi": { 370 | "version": "7.0.0", 371 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 372 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 373 | "dev": true, 374 | "license": "MIT", 375 | "dependencies": { 376 | "ansi-styles": "^4.0.0", 377 | "string-width": "^4.1.0", 378 | "strip-ansi": "^6.0.0" 379 | }, 380 | "engines": { 381 | "node": ">=10" 382 | }, 383 | "funding": { 384 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 385 | } 386 | }, 387 | "node_modules/color-convert": { 388 | "version": "2.0.1", 389 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", 390 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", 391 | "dev": true, 392 | "license": "MIT", 393 | "dependencies": { 394 | "color-name": "~1.1.4" 395 | }, 396 | "engines": { 397 | "node": ">=7.0.0" 398 | } 399 | }, 400 | "node_modules/color-name": { 401 | "version": "1.1.4", 402 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", 403 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", 404 | "dev": true, 405 | "license": "MIT" 406 | }, 407 | "node_modules/cross-spawn": { 408 | "version": "7.0.6", 409 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", 410 | "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", 411 | "dev": true, 412 | "license": "MIT", 413 | "dependencies": { 414 | "path-key": "^3.1.0", 415 | "shebang-command": "^2.0.0", 416 | "which": "^2.0.1" 417 | }, 418 | "engines": { 419 | "node": ">= 8" 420 | } 421 | }, 422 | "node_modules/debug": { 423 | "version": "4.4.1", 424 | "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", 425 | "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", 426 | "license": "MIT", 427 | "dependencies": { 428 | "ms": "^2.1.3" 429 | }, 430 | "engines": { 431 | "node": ">=6.0" 432 | }, 433 | "peerDependenciesMeta": { 434 | "supports-color": { 435 | "optional": true 436 | } 437 | } 438 | }, 439 | "node_modules/decamelize": { 440 | "version": "4.0.0", 441 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", 442 | "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", 443 | "dev": true, 444 | "license": "MIT", 445 | "engines": { 446 | "node": ">=10" 447 | }, 448 | "funding": { 449 | "url": "https://github.com/sponsors/sindresorhus" 450 | } 451 | }, 452 | "node_modules/deep-eql": { 453 | "version": "5.0.2", 454 | "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", 455 | "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", 456 | "dev": true, 457 | "license": "MIT", 458 | "engines": { 459 | "node": ">=6" 460 | } 461 | }, 462 | "node_modules/diff": { 463 | "version": "7.0.0", 464 | "resolved": "https://registry.npmjs.org/diff/-/diff-7.0.0.tgz", 465 | "integrity": "sha512-PJWHUb1RFevKCwaFA9RlG5tCd+FO5iRh9A8HEtkmBH2Li03iJriB6m6JIN4rGz3K3JLawI7/veA1xzRKP6ISBw==", 466 | "dev": true, 467 | "license": "BSD-3-Clause", 468 | "engines": { 469 | "node": ">=0.3.1" 470 | } 471 | }, 472 | "node_modules/eastasianwidth": { 473 | "version": "0.2.0", 474 | "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", 475 | "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", 476 | "dev": true, 477 | "license": "MIT" 478 | }, 479 | "node_modules/emoji-regex": { 480 | "version": "9.2.2", 481 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", 482 | "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", 483 | "dev": true, 484 | "license": "MIT" 485 | }, 486 | "node_modules/escalade": { 487 | "version": "3.2.0", 488 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", 489 | "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", 490 | "dev": true, 491 | "license": "MIT", 492 | "engines": { 493 | "node": ">=6" 494 | } 495 | }, 496 | "node_modules/escape-string-regexp": { 497 | "version": "4.0.0", 498 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", 499 | "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", 500 | "dev": true, 501 | "license": "MIT", 502 | "engines": { 503 | "node": ">=10" 504 | }, 505 | "funding": { 506 | "url": "https://github.com/sponsors/sindresorhus" 507 | } 508 | }, 509 | "node_modules/find-up": { 510 | "version": "5.0.0", 511 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", 512 | "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", 513 | "dev": true, 514 | "license": "MIT", 515 | "dependencies": { 516 | "locate-path": "^6.0.0", 517 | "path-exists": "^4.0.0" 518 | }, 519 | "engines": { 520 | "node": ">=10" 521 | }, 522 | "funding": { 523 | "url": "https://github.com/sponsors/sindresorhus" 524 | } 525 | }, 526 | "node_modules/flat": { 527 | "version": "5.0.2", 528 | "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", 529 | "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", 530 | "dev": true, 531 | "license": "BSD-3-Clause", 532 | "bin": { 533 | "flat": "cli.js" 534 | } 535 | }, 536 | "node_modules/foreground-child": { 537 | "version": "3.3.1", 538 | "resolved": "https://registry.npmjs.org/foreground-child/-/foreground-child-3.3.1.tgz", 539 | "integrity": "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==", 540 | "dev": true, 541 | "license": "ISC", 542 | "dependencies": { 543 | "cross-spawn": "^7.0.6", 544 | "signal-exit": "^4.0.1" 545 | }, 546 | "engines": { 547 | "node": ">=14" 548 | }, 549 | "funding": { 550 | "url": "https://github.com/sponsors/isaacs" 551 | } 552 | }, 553 | "node_modules/get-caller-file": { 554 | "version": "2.0.5", 555 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", 556 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", 557 | "dev": true, 558 | "license": "ISC", 559 | "engines": { 560 | "node": "6.* || 8.* || >= 10.*" 561 | } 562 | }, 563 | "node_modules/glob": { 564 | "version": "10.4.5", 565 | "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", 566 | "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", 567 | "dev": true, 568 | "license": "ISC", 569 | "dependencies": { 570 | "foreground-child": "^3.1.0", 571 | "jackspeak": "^3.1.2", 572 | "minimatch": "^9.0.4", 573 | "minipass": "^7.1.2", 574 | "package-json-from-dist": "^1.0.0", 575 | "path-scurry": "^1.11.1" 576 | }, 577 | "bin": { 578 | "glob": "dist/esm/bin.mjs" 579 | }, 580 | "funding": { 581 | "url": "https://github.com/sponsors/isaacs" 582 | } 583 | }, 584 | "node_modules/has-flag": { 585 | "version": "4.0.0", 586 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", 587 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", 588 | "dev": true, 589 | "license": "MIT", 590 | "engines": { 591 | "node": ">=8" 592 | } 593 | }, 594 | "node_modules/he": { 595 | "version": "1.2.0", 596 | "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", 597 | "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", 598 | "dev": true, 599 | "license": "MIT", 600 | "bin": { 601 | "he": "bin/he" 602 | } 603 | }, 604 | "node_modules/is-fullwidth-code-point": { 605 | "version": "3.0.0", 606 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", 607 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", 608 | "dev": true, 609 | "license": "MIT", 610 | "engines": { 611 | "node": ">=8" 612 | } 613 | }, 614 | "node_modules/is-plain-obj": { 615 | "version": "2.1.0", 616 | "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", 617 | "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", 618 | "dev": true, 619 | "license": "MIT", 620 | "engines": { 621 | "node": ">=8" 622 | } 623 | }, 624 | "node_modules/is-unicode-supported": { 625 | "version": "0.1.0", 626 | "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", 627 | "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", 628 | "dev": true, 629 | "license": "MIT", 630 | "engines": { 631 | "node": ">=10" 632 | }, 633 | "funding": { 634 | "url": "https://github.com/sponsors/sindresorhus" 635 | } 636 | }, 637 | "node_modules/isexe": { 638 | "version": "2.0.0", 639 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 640 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", 641 | "dev": true, 642 | "license": "ISC" 643 | }, 644 | "node_modules/jackspeak": { 645 | "version": "3.4.3", 646 | "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-3.4.3.tgz", 647 | "integrity": "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw==", 648 | "dev": true, 649 | "license": "BlueOak-1.0.0", 650 | "dependencies": { 651 | "@isaacs/cliui": "^8.0.2" 652 | }, 653 | "funding": { 654 | "url": "https://github.com/sponsors/isaacs" 655 | }, 656 | "optionalDependencies": { 657 | "@pkgjs/parseargs": "^0.11.0" 658 | } 659 | }, 660 | "node_modules/js-yaml": { 661 | "version": "4.1.0", 662 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", 663 | "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", 664 | "dev": true, 665 | "license": "MIT", 666 | "dependencies": { 667 | "argparse": "^2.0.1" 668 | }, 669 | "bin": { 670 | "js-yaml": "bin/js-yaml.js" 671 | } 672 | }, 673 | "node_modules/locate-path": { 674 | "version": "6.0.0", 675 | "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", 676 | "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", 677 | "dev": true, 678 | "license": "MIT", 679 | "dependencies": { 680 | "p-locate": "^5.0.0" 681 | }, 682 | "engines": { 683 | "node": ">=10" 684 | }, 685 | "funding": { 686 | "url": "https://github.com/sponsors/sindresorhus" 687 | } 688 | }, 689 | "node_modules/lodash": { 690 | "version": "4.17.21", 691 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", 692 | "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", 693 | "license": "MIT" 694 | }, 695 | "node_modules/lodash.get": { 696 | "version": "4.4.2", 697 | "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", 698 | "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", 699 | "deprecated": "This package is deprecated. Use the optional chaining (?.) operator instead.", 700 | "dev": true, 701 | "license": "MIT" 702 | }, 703 | "node_modules/log-symbols": { 704 | "version": "4.1.0", 705 | "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", 706 | "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", 707 | "dev": true, 708 | "license": "MIT", 709 | "dependencies": { 710 | "chalk": "^4.1.0", 711 | "is-unicode-supported": "^0.1.0" 712 | }, 713 | "engines": { 714 | "node": ">=10" 715 | }, 716 | "funding": { 717 | "url": "https://github.com/sponsors/sindresorhus" 718 | } 719 | }, 720 | "node_modules/loupe": { 721 | "version": "3.1.3", 722 | "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.3.tgz", 723 | "integrity": "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug==", 724 | "dev": true, 725 | "license": "MIT" 726 | }, 727 | "node_modules/minimatch": { 728 | "version": "9.0.5", 729 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", 730 | "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", 731 | "dev": true, 732 | "license": "ISC", 733 | "dependencies": { 734 | "brace-expansion": "^2.0.1" 735 | }, 736 | "engines": { 737 | "node": ">=16 || 14 >=14.17" 738 | }, 739 | "funding": { 740 | "url": "https://github.com/sponsors/isaacs" 741 | } 742 | }, 743 | "node_modules/minipass": { 744 | "version": "7.1.2", 745 | "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", 746 | "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", 747 | "dev": true, 748 | "license": "ISC", 749 | "engines": { 750 | "node": ">=16 || 14 >=14.17" 751 | } 752 | }, 753 | "node_modules/mocha": { 754 | "version": "11.5.0", 755 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-11.5.0.tgz", 756 | "integrity": "sha512-VKDjhy6LMTKm0WgNEdlY77YVsD49LZnPSXJAaPNL9NRYQADxvORsyG1DIQY6v53BKTnlNbEE2MbVCDbnxr4K3w==", 757 | "dev": true, 758 | "license": "MIT", 759 | "dependencies": { 760 | "browser-stdout": "^1.3.1", 761 | "chokidar": "^4.0.1", 762 | "debug": "^4.3.5", 763 | "diff": "^7.0.0", 764 | "escape-string-regexp": "^4.0.0", 765 | "find-up": "^5.0.0", 766 | "glob": "^10.4.5", 767 | "he": "^1.2.0", 768 | "js-yaml": "^4.1.0", 769 | "log-symbols": "^4.1.0", 770 | "minimatch": "^9.0.5", 771 | "ms": "^2.1.3", 772 | "picocolors": "^1.1.1", 773 | "serialize-javascript": "^6.0.2", 774 | "strip-json-comments": "^3.1.1", 775 | "supports-color": "^8.1.1", 776 | "workerpool": "^6.5.1", 777 | "yargs": "^17.7.2", 778 | "yargs-parser": "^21.1.1", 779 | "yargs-unparser": "^2.0.0" 780 | }, 781 | "bin": { 782 | "_mocha": "bin/_mocha", 783 | "mocha": "bin/mocha.js" 784 | }, 785 | "engines": { 786 | "node": "^18.18.0 || ^20.9.0 || >=21.1.0" 787 | } 788 | }, 789 | "node_modules/ms": { 790 | "version": "2.1.3", 791 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", 792 | "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", 793 | "license": "MIT" 794 | }, 795 | "node_modules/p-limit": { 796 | "version": "3.1.0", 797 | "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", 798 | "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", 799 | "dev": true, 800 | "license": "MIT", 801 | "dependencies": { 802 | "yocto-queue": "^0.1.0" 803 | }, 804 | "engines": { 805 | "node": ">=10" 806 | }, 807 | "funding": { 808 | "url": "https://github.com/sponsors/sindresorhus" 809 | } 810 | }, 811 | "node_modules/p-locate": { 812 | "version": "5.0.0", 813 | "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", 814 | "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", 815 | "dev": true, 816 | "license": "MIT", 817 | "dependencies": { 818 | "p-limit": "^3.0.2" 819 | }, 820 | "engines": { 821 | "node": ">=10" 822 | }, 823 | "funding": { 824 | "url": "https://github.com/sponsors/sindresorhus" 825 | } 826 | }, 827 | "node_modules/package-json-from-dist": { 828 | "version": "1.0.1", 829 | "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz", 830 | "integrity": "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==", 831 | "dev": true, 832 | "license": "BlueOak-1.0.0" 833 | }, 834 | "node_modules/path-exists": { 835 | "version": "4.0.0", 836 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", 837 | "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", 838 | "dev": true, 839 | "license": "MIT", 840 | "engines": { 841 | "node": ">=8" 842 | } 843 | }, 844 | "node_modules/path-key": { 845 | "version": "3.1.1", 846 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", 847 | "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", 848 | "dev": true, 849 | "license": "MIT", 850 | "engines": { 851 | "node": ">=8" 852 | } 853 | }, 854 | "node_modules/path-scurry": { 855 | "version": "1.11.1", 856 | "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", 857 | "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", 858 | "dev": true, 859 | "license": "BlueOak-1.0.0", 860 | "dependencies": { 861 | "lru-cache": "^10.2.0", 862 | "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" 863 | }, 864 | "engines": { 865 | "node": ">=16 || 14 >=14.18" 866 | }, 867 | "funding": { 868 | "url": "https://github.com/sponsors/isaacs" 869 | } 870 | }, 871 | "node_modules/path-scurry/node_modules/lru-cache": { 872 | "version": "10.4.3", 873 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", 874 | "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", 875 | "dev": true, 876 | "license": "ISC" 877 | }, 878 | "node_modules/pathval": { 879 | "version": "2.0.0", 880 | "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", 881 | "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", 882 | "dev": true, 883 | "license": "MIT", 884 | "engines": { 885 | "node": ">= 14.16" 886 | } 887 | }, 888 | "node_modules/picocolors": { 889 | "version": "1.1.1", 890 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", 891 | "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", 892 | "dev": true, 893 | "license": "ISC" 894 | }, 895 | "node_modules/promise": { 896 | "version": "3.2.0", 897 | "resolved": "https://registry.npmjs.org/promise/-/promise-3.2.0.tgz", 898 | "integrity": "sha1-tND6KBvNXKnWreVWtp3VlHuau5Q=" 899 | }, 900 | "node_modules/q": { 901 | "version": "1.4.1", 902 | "resolved": "https://registry.npmjs.org/q/-/q-1.4.1.tgz", 903 | "integrity": "sha1-VXBbzZPF82c1MMLCy8DCs63cKG4=", 904 | "engines": { 905 | "node": ">=0.6.0", 906 | "teleport": ">=0.2.0" 907 | } 908 | }, 909 | "node_modules/querystringify": { 910 | "version": "2.2.0", 911 | "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", 912 | "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", 913 | "license": "MIT" 914 | }, 915 | "node_modules/randombytes": { 916 | "version": "2.1.0", 917 | "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", 918 | "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", 919 | "dev": true, 920 | "license": "MIT", 921 | "dependencies": { 922 | "safe-buffer": "^5.1.0" 923 | } 924 | }, 925 | "node_modules/readdirp": { 926 | "version": "4.1.2", 927 | "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", 928 | "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", 929 | "dev": true, 930 | "license": "MIT", 931 | "engines": { 932 | "node": ">= 14.18.0" 933 | }, 934 | "funding": { 935 | "type": "individual", 936 | "url": "https://paulmillr.com/funding/" 937 | } 938 | }, 939 | "node_modules/require-directory": { 940 | "version": "2.1.1", 941 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 942 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 943 | "dev": true, 944 | "license": "MIT", 945 | "engines": { 946 | "node": ">=0.10.0" 947 | } 948 | }, 949 | "node_modules/requires-port": { 950 | "version": "1.0.0", 951 | "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", 952 | "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", 953 | "license": "MIT" 954 | }, 955 | "node_modules/safe-buffer": { 956 | "version": "5.2.1", 957 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", 958 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", 959 | "dev": true, 960 | "funding": [ 961 | { 962 | "type": "github", 963 | "url": "https://github.com/sponsors/feross" 964 | }, 965 | { 966 | "type": "patreon", 967 | "url": "https://www.patreon.com/feross" 968 | }, 969 | { 970 | "type": "consulting", 971 | "url": "https://feross.org/support" 972 | } 973 | ], 974 | "license": "MIT" 975 | }, 976 | "node_modules/serialize-javascript": { 977 | "version": "6.0.2", 978 | "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", 979 | "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", 980 | "dev": true, 981 | "license": "BSD-3-Clause", 982 | "dependencies": { 983 | "randombytes": "^2.1.0" 984 | } 985 | }, 986 | "node_modules/shebang-command": { 987 | "version": "2.0.0", 988 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 989 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 990 | "dev": true, 991 | "license": "MIT", 992 | "dependencies": { 993 | "shebang-regex": "^3.0.0" 994 | }, 995 | "engines": { 996 | "node": ">=8" 997 | } 998 | }, 999 | "node_modules/shebang-regex": { 1000 | "version": "3.0.0", 1001 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 1002 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", 1003 | "dev": true, 1004 | "license": "MIT", 1005 | "engines": { 1006 | "node": ">=8" 1007 | } 1008 | }, 1009 | "node_modules/signal-exit": { 1010 | "version": "4.1.0", 1011 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", 1012 | "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", 1013 | "dev": true, 1014 | "license": "ISC", 1015 | "engines": { 1016 | "node": ">=14" 1017 | }, 1018 | "funding": { 1019 | "url": "https://github.com/sponsors/isaacs" 1020 | } 1021 | }, 1022 | "node_modules/sinon": { 1023 | "version": "20.0.0", 1024 | "resolved": "https://registry.npmjs.org/sinon/-/sinon-20.0.0.tgz", 1025 | "integrity": "sha512-+FXOAbdnj94AQIxH0w1v8gzNxkawVvNqE3jUzRLptR71Oykeu2RrQXXl/VQjKay+Qnh73fDt/oDfMo6xMeDQbQ==", 1026 | "dev": true, 1027 | "license": "BSD-3-Clause", 1028 | "dependencies": { 1029 | "@sinonjs/commons": "^3.0.1", 1030 | "@sinonjs/fake-timers": "^13.0.5", 1031 | "@sinonjs/samsam": "^8.0.1", 1032 | "diff": "^7.0.0", 1033 | "supports-color": "^7.2.0" 1034 | }, 1035 | "funding": { 1036 | "type": "opencollective", 1037 | "url": "https://opencollective.com/sinon" 1038 | } 1039 | }, 1040 | "node_modules/sinon-chai": { 1041 | "version": "4.0.0", 1042 | "resolved": "https://registry.npmjs.org/sinon-chai/-/sinon-chai-4.0.0.tgz", 1043 | "integrity": "sha512-cWqO7O2I4XfJDWyWElAQ9D/dtdh5Mo0RHndsfiiYyjWnlPzBJdIvjCVURO4EjyYaC3BjV+ISNXCfTXPXTEIEWA==", 1044 | "dev": true, 1045 | "license": "(BSD-2-Clause OR WTFPL)", 1046 | "peerDependencies": { 1047 | "chai": "^5.0.0", 1048 | "sinon": ">=4.0.0" 1049 | } 1050 | }, 1051 | "node_modules/sinon/node_modules/supports-color": { 1052 | "version": "7.2.0", 1053 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", 1054 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", 1055 | "dev": true, 1056 | "license": "MIT", 1057 | "dependencies": { 1058 | "has-flag": "^4.0.0" 1059 | }, 1060 | "engines": { 1061 | "node": ">=8" 1062 | } 1063 | }, 1064 | "node_modules/string-width": { 1065 | "version": "5.1.2", 1066 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", 1067 | "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", 1068 | "dev": true, 1069 | "license": "MIT", 1070 | "dependencies": { 1071 | "eastasianwidth": "^0.2.0", 1072 | "emoji-regex": "^9.2.2", 1073 | "strip-ansi": "^7.0.1" 1074 | }, 1075 | "engines": { 1076 | "node": ">=12" 1077 | }, 1078 | "funding": { 1079 | "url": "https://github.com/sponsors/sindresorhus" 1080 | } 1081 | }, 1082 | "node_modules/string-width-cjs": { 1083 | "name": "string-width", 1084 | "version": "4.2.3", 1085 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1086 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1087 | "dev": true, 1088 | "license": "MIT", 1089 | "dependencies": { 1090 | "emoji-regex": "^8.0.0", 1091 | "is-fullwidth-code-point": "^3.0.0", 1092 | "strip-ansi": "^6.0.1" 1093 | }, 1094 | "engines": { 1095 | "node": ">=8" 1096 | } 1097 | }, 1098 | "node_modules/string-width-cjs/node_modules/ansi-regex": { 1099 | "version": "5.0.1", 1100 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1101 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1102 | "dev": true, 1103 | "license": "MIT", 1104 | "engines": { 1105 | "node": ">=8" 1106 | } 1107 | }, 1108 | "node_modules/string-width-cjs/node_modules/emoji-regex": { 1109 | "version": "8.0.0", 1110 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1111 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1112 | "dev": true, 1113 | "license": "MIT" 1114 | }, 1115 | "node_modules/string-width-cjs/node_modules/strip-ansi": { 1116 | "version": "6.0.1", 1117 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1118 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1119 | "dev": true, 1120 | "license": "MIT", 1121 | "dependencies": { 1122 | "ansi-regex": "^5.0.1" 1123 | }, 1124 | "engines": { 1125 | "node": ">=8" 1126 | } 1127 | }, 1128 | "node_modules/strip-ansi": { 1129 | "version": "7.1.0", 1130 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", 1131 | "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", 1132 | "dev": true, 1133 | "license": "MIT", 1134 | "dependencies": { 1135 | "ansi-regex": "^6.0.1" 1136 | }, 1137 | "engines": { 1138 | "node": ">=12" 1139 | }, 1140 | "funding": { 1141 | "url": "https://github.com/chalk/strip-ansi?sponsor=1" 1142 | } 1143 | }, 1144 | "node_modules/strip-ansi-cjs": { 1145 | "name": "strip-ansi", 1146 | "version": "6.0.1", 1147 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1148 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1149 | "dev": true, 1150 | "license": "MIT", 1151 | "dependencies": { 1152 | "ansi-regex": "^5.0.1" 1153 | }, 1154 | "engines": { 1155 | "node": ">=8" 1156 | } 1157 | }, 1158 | "node_modules/strip-ansi-cjs/node_modules/ansi-regex": { 1159 | "version": "5.0.1", 1160 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1161 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1162 | "dev": true, 1163 | "license": "MIT", 1164 | "engines": { 1165 | "node": ">=8" 1166 | } 1167 | }, 1168 | "node_modules/strip-json-comments": { 1169 | "version": "3.1.1", 1170 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", 1171 | "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", 1172 | "dev": true, 1173 | "license": "MIT", 1174 | "engines": { 1175 | "node": ">=8" 1176 | }, 1177 | "funding": { 1178 | "url": "https://github.com/sponsors/sindresorhus" 1179 | } 1180 | }, 1181 | "node_modules/supports-color": { 1182 | "version": "8.1.1", 1183 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", 1184 | "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", 1185 | "dev": true, 1186 | "license": "MIT", 1187 | "dependencies": { 1188 | "has-flag": "^4.0.0" 1189 | }, 1190 | "engines": { 1191 | "node": ">=10" 1192 | }, 1193 | "funding": { 1194 | "url": "https://github.com/chalk/supports-color?sponsor=1" 1195 | } 1196 | }, 1197 | "node_modules/throat": { 1198 | "version": "1.0.0", 1199 | "resolved": "https://registry.npmjs.org/throat/-/throat-1.0.0.tgz", 1200 | "integrity": "sha1-BMng+c6I4lDbYw/eq8LluxUqBiU=", 1201 | "dependencies": { 1202 | "promise": "~3.2.0" 1203 | } 1204 | }, 1205 | "node_modules/type-detect": { 1206 | "version": "4.0.8", 1207 | "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", 1208 | "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", 1209 | "dev": true, 1210 | "license": "MIT", 1211 | "engines": { 1212 | "node": ">=4" 1213 | } 1214 | }, 1215 | "node_modules/url-parse": { 1216 | "version": "1.5.10", 1217 | "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.10.tgz", 1218 | "integrity": "sha512-WypcfiRhfeUP9vvF0j6rw0J3hrWrw6iZv3+22h6iRMJ/8z1Tj6XfLP4DsUix5MhMPnXpiHDoKyoZ/bdCkwBCiQ==", 1219 | "license": "MIT", 1220 | "dependencies": { 1221 | "querystringify": "^2.1.1", 1222 | "requires-port": "^1.0.0" 1223 | } 1224 | }, 1225 | "node_modules/uuid": { 1226 | "version": "11.1.0", 1227 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", 1228 | "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", 1229 | "funding": [ 1230 | "https://github.com/sponsors/broofa", 1231 | "https://github.com/sponsors/ctavan" 1232 | ], 1233 | "license": "MIT", 1234 | "bin": { 1235 | "uuid": "dist/esm/bin/uuid" 1236 | } 1237 | }, 1238 | "node_modules/which": { 1239 | "version": "2.0.2", 1240 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", 1241 | "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", 1242 | "dev": true, 1243 | "license": "ISC", 1244 | "dependencies": { 1245 | "isexe": "^2.0.0" 1246 | }, 1247 | "bin": { 1248 | "node-which": "bin/node-which" 1249 | }, 1250 | "engines": { 1251 | "node": ">= 8" 1252 | } 1253 | }, 1254 | "node_modules/workerpool": { 1255 | "version": "6.5.1", 1256 | "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.5.1.tgz", 1257 | "integrity": "sha512-Fs4dNYcsdpYSAfVxhnl1L5zTksjvOJxtC5hzMNl+1t9B8hTJTdKDyZ5ju7ztgPy+ft9tBFXoOlDNiOT9WUXZlA==", 1258 | "dev": true, 1259 | "license": "Apache-2.0" 1260 | }, 1261 | "node_modules/wrap-ansi": { 1262 | "version": "8.1.0", 1263 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", 1264 | "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", 1265 | "dev": true, 1266 | "license": "MIT", 1267 | "dependencies": { 1268 | "ansi-styles": "^6.1.0", 1269 | "string-width": "^5.0.1", 1270 | "strip-ansi": "^7.0.1" 1271 | }, 1272 | "engines": { 1273 | "node": ">=12" 1274 | }, 1275 | "funding": { 1276 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1277 | } 1278 | }, 1279 | "node_modules/wrap-ansi-cjs": { 1280 | "name": "wrap-ansi", 1281 | "version": "7.0.0", 1282 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", 1283 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", 1284 | "dev": true, 1285 | "license": "MIT", 1286 | "dependencies": { 1287 | "ansi-styles": "^4.0.0", 1288 | "string-width": "^4.1.0", 1289 | "strip-ansi": "^6.0.0" 1290 | }, 1291 | "engines": { 1292 | "node": ">=10" 1293 | }, 1294 | "funding": { 1295 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1" 1296 | } 1297 | }, 1298 | "node_modules/wrap-ansi-cjs/node_modules/ansi-regex": { 1299 | "version": "5.0.1", 1300 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1301 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1302 | "dev": true, 1303 | "license": "MIT", 1304 | "engines": { 1305 | "node": ">=8" 1306 | } 1307 | }, 1308 | "node_modules/wrap-ansi-cjs/node_modules/emoji-regex": { 1309 | "version": "8.0.0", 1310 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1311 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1312 | "dev": true, 1313 | "license": "MIT" 1314 | }, 1315 | "node_modules/wrap-ansi-cjs/node_modules/string-width": { 1316 | "version": "4.2.3", 1317 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1318 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1319 | "dev": true, 1320 | "license": "MIT", 1321 | "dependencies": { 1322 | "emoji-regex": "^8.0.0", 1323 | "is-fullwidth-code-point": "^3.0.0", 1324 | "strip-ansi": "^6.0.1" 1325 | }, 1326 | "engines": { 1327 | "node": ">=8" 1328 | } 1329 | }, 1330 | "node_modules/wrap-ansi-cjs/node_modules/strip-ansi": { 1331 | "version": "6.0.1", 1332 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1333 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1334 | "dev": true, 1335 | "license": "MIT", 1336 | "dependencies": { 1337 | "ansi-regex": "^5.0.1" 1338 | }, 1339 | "engines": { 1340 | "node": ">=8" 1341 | } 1342 | }, 1343 | "node_modules/wrap-ansi/node_modules/ansi-styles": { 1344 | "version": "6.2.1", 1345 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", 1346 | "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", 1347 | "dev": true, 1348 | "license": "MIT", 1349 | "engines": { 1350 | "node": ">=12" 1351 | }, 1352 | "funding": { 1353 | "url": "https://github.com/chalk/ansi-styles?sponsor=1" 1354 | } 1355 | }, 1356 | "node_modules/y18n": { 1357 | "version": "5.0.8", 1358 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", 1359 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", 1360 | "dev": true, 1361 | "license": "ISC", 1362 | "engines": { 1363 | "node": ">=10" 1364 | } 1365 | }, 1366 | "node_modules/yargs": { 1367 | "version": "17.7.2", 1368 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", 1369 | "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", 1370 | "dev": true, 1371 | "license": "MIT", 1372 | "dependencies": { 1373 | "cliui": "^8.0.1", 1374 | "escalade": "^3.1.1", 1375 | "get-caller-file": "^2.0.5", 1376 | "require-directory": "^2.1.1", 1377 | "string-width": "^4.2.3", 1378 | "y18n": "^5.0.5", 1379 | "yargs-parser": "^21.1.1" 1380 | }, 1381 | "engines": { 1382 | "node": ">=12" 1383 | } 1384 | }, 1385 | "node_modules/yargs-parser": { 1386 | "version": "21.1.1", 1387 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", 1388 | "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", 1389 | "dev": true, 1390 | "license": "ISC", 1391 | "engines": { 1392 | "node": ">=12" 1393 | } 1394 | }, 1395 | "node_modules/yargs-unparser": { 1396 | "version": "2.0.0", 1397 | "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", 1398 | "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", 1399 | "dev": true, 1400 | "license": "MIT", 1401 | "dependencies": { 1402 | "camelcase": "^6.0.0", 1403 | "decamelize": "^4.0.0", 1404 | "flat": "^5.0.2", 1405 | "is-plain-obj": "^2.1.0" 1406 | }, 1407 | "engines": { 1408 | "node": ">=10" 1409 | } 1410 | }, 1411 | "node_modules/yargs/node_modules/ansi-regex": { 1412 | "version": "5.0.1", 1413 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", 1414 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", 1415 | "dev": true, 1416 | "license": "MIT", 1417 | "engines": { 1418 | "node": ">=8" 1419 | } 1420 | }, 1421 | "node_modules/yargs/node_modules/emoji-regex": { 1422 | "version": "8.0.0", 1423 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", 1424 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", 1425 | "dev": true, 1426 | "license": "MIT" 1427 | }, 1428 | "node_modules/yargs/node_modules/string-width": { 1429 | "version": "4.2.3", 1430 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", 1431 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", 1432 | "dev": true, 1433 | "license": "MIT", 1434 | "dependencies": { 1435 | "emoji-regex": "^8.0.0", 1436 | "is-fullwidth-code-point": "^3.0.0", 1437 | "strip-ansi": "^6.0.1" 1438 | }, 1439 | "engines": { 1440 | "node": ">=8" 1441 | } 1442 | }, 1443 | "node_modules/yargs/node_modules/strip-ansi": { 1444 | "version": "6.0.1", 1445 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", 1446 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", 1447 | "dev": true, 1448 | "license": "MIT", 1449 | "dependencies": { 1450 | "ansi-regex": "^5.0.1" 1451 | }, 1452 | "engines": { 1453 | "node": ">=8" 1454 | } 1455 | }, 1456 | "node_modules/yocto-queue": { 1457 | "version": "0.1.0", 1458 | "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", 1459 | "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", 1460 | "dev": true, 1461 | "license": "MIT", 1462 | "engines": { 1463 | "node": ">=10" 1464 | }, 1465 | "funding": { 1466 | "url": "https://github.com/sponsors/sindresorhus" 1467 | } 1468 | } 1469 | } 1470 | } 1471 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "microservice-kit", 3 | "version": "0.9.1", 4 | "description": "Utility belt for building microservices", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "test": "./node_modules/.bin/mocha" 8 | }, 9 | "author": "OmmaSign Tech ", 10 | "license": "ISC", 11 | "dependencies": { 12 | "amqplib": "0.10.8", 13 | "async": "2.6.1", 14 | "async-q": "0.3.1", 15 | "chance": "1.0.10", 16 | "debug": "4.4.1", 17 | "lodash": "4.17.21", 18 | "uuid": "11.1.0" 19 | }, 20 | "devDependencies": { 21 | "chai": "5.2.0", 22 | "chai-as-promised": "8.0.1", 23 | "mocha": "11.5.0", 24 | "sinon": "20.0.0", 25 | "sinon-chai": "4.0.0" 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/amqpkit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const async = require('async-q'); 4 | const _ = require('lodash'); 5 | const amqp = require('amqplib'); 6 | const EventEmitterExtra = require('./lib/event-emitter-extra'); 7 | const uuid = require('uuid'); 8 | const debug = require('debug')('microservice-kit:amqpkit'); 9 | const url = require('url'); 10 | 11 | const Message = require('./lib/message'); 12 | const Response = require('./lib/response'); 13 | const Router = require('./lib/router'); 14 | const Queue = require('./lib/queue'); 15 | const Exchange = require('./lib/exchange'); 16 | const RPC = require('./lib/rpc'); 17 | const ShutdownKit = require('./shutdownkit'); 18 | 19 | 20 | class AmqpKit extends EventEmitterExtra { 21 | /** 22 | * @param {Object=} opt_options 23 | * url, rpc, queues, exchanges 24 | */ 25 | constructor(opt_options) { 26 | super(); 27 | this.options_ = _.assign({}, this.defaults, opt_options || {}); 28 | 29 | this.connection = null; 30 | this.channel = null; 31 | this.rpc_ = null; 32 | this.queues_ = {}; 33 | this.exchanges_ = {}; 34 | } 35 | 36 | 37 | /** 38 | * Connects to rabbitmq, creates channel and creates rpc queue if needed. 39 | * @return {Promise.} 40 | */ 41 | init() { 42 | if (this.options_.exchanges && !Array.isArray(this.options_.exchanges)) 43 | throw new Error('MicroserviceKit init failed. ' + 44 | 'options.exchanges must be an array.'); 45 | 46 | if (this.options_.queues && !Array.isArray(this.options_.queues)) 47 | throw new Error('MicroserviceKit init failed. ' + 48 | 'options.queues must be an array.'); 49 | 50 | if (this.options_.url) { 51 | this.options_.connectionOptions = _.assign(this.options_.connectionOptions, { 52 | servername: url.parse(this.options_.url).hostname 53 | }); 54 | } 55 | 56 | return amqp 57 | .connect(this.options_.url, this.options_.connectionOptions) 58 | .then((connection) => { 59 | this.connection = connection; 60 | var jobs = [ 61 | connection.createChannel() 62 | ]; 63 | 64 | if (this.options_.rpc) { 65 | this.rpc_ = new RPC(); 66 | this.rpc_.on('log', (...args) => this.emit('log', ...args)); 67 | 68 | const rpcQueueName = this.options_.id + '-rpc'; 69 | jobs.push(this.rpc_.init(connection, rpcQueueName)); 70 | } 71 | 72 | return Promise.all(jobs); 73 | }) 74 | .then((channels) => { 75 | this.channel = channels[0]; 76 | this.bindEvents(); 77 | return this; 78 | }) 79 | .then(() => { 80 | const queues = this.options_.queues || []; 81 | debug('info', 'Asserting ' + queues.length + ' queues'); 82 | return async.mapLimit(queues, 5, (item, index) => { 83 | return this.createQueue(item.key, item.name, item.options); 84 | }) 85 | }) 86 | .then(() => { 87 | const exchanges = this.options_.exchanges || []; 88 | debug('info', 'Asserting ' + exchanges.length + ' exchanges'); 89 | return async.mapLimit(exchanges, 5, (item, index) => { 90 | return this.createExchange(item.key, item.name, item.type, item.options); 91 | }) 92 | }); 93 | } 94 | 95 | 96 | /** 97 | * Bind rabbitmq's connection events. 98 | */ 99 | bindEvents() { 100 | this.connection.on('close', () => { 101 | debug('error', 'amqp connection closed'); 102 | ShutdownKit.gracefulShutdown(); 103 | }); 104 | 105 | this.connection.on('error', (err) => { 106 | debug('error', 'amqp connection error', err && err.stack ? err.stack : err); 107 | }); 108 | 109 | this.connection.on('blocked', () => { 110 | debug('error', 'amqp connection blocked'); 111 | }); 112 | 113 | this.connection.on('unblocked', () => { 114 | debug('info', 'amqp connection unblocked'); 115 | }); 116 | 117 | ShutdownKit.addJob((done) => { 118 | debug('info', 'Closing amqp connection...'); 119 | try { 120 | this.connection 121 | .close() 122 | .then(() => { 123 | done(); 124 | }) 125 | .catch(done); 126 | } catch (err) { 127 | debug('error', 'Could not close connection', err); 128 | done(); 129 | } 130 | }); 131 | } 132 | 133 | 134 | /** 135 | * prefetch wrapper function. 136 | */ 137 | prefetch(count, opt_global) { 138 | return this.channel.prefetch(count, opt_global); 139 | } 140 | 141 | 142 | 143 | /** 144 | * Returns queue by key 145 | * @param {string} queueKey 146 | */ 147 | getQueue(queueKey) { 148 | return this.queues_[queueKey]; 149 | } 150 | 151 | 152 | /** 153 | * Returns echange by key 154 | * @param {string} exchangeKey 155 | */ 156 | getExchange(exchangeKey) { 157 | return this.exchanges_[exchangeKey]; 158 | } 159 | 160 | 161 | /** 162 | * Creates a queue. 163 | * @param {string} key 164 | * @param {string} name 165 | * @param {Object=} opt_options 166 | * @return {Promise} 167 | */ 168 | createQueue(key, name, opt_options) { 169 | if (!key) 170 | return Promise.reject(new Error('You cannot create queue without key.')); 171 | 172 | if (this.queues_[key]) 173 | return Promise.reject(new Error('You cannot create queue with same key more than once.')); 174 | 175 | if (!name && opt_options && opt_options.exclusive) 176 | name = this.options_.id + '-' + 'excl' + '-' + uuid.v4().split('-')[0]; 177 | 178 | const queue = new Queue({ 179 | channel: this.channel, 180 | name: name, 181 | options: opt_options, 182 | rpc: this.rpc_, 183 | tracer: this.options_.tracer 184 | }); 185 | 186 | queue.on('log', (...args) => this.emit('log', ...args)); 187 | queue.on('consumedEvent', payload => this.emit('consumedEvent', payload)); 188 | 189 | return queue.init() 190 | .then(() => { 191 | this.queues_[key] = queue; 192 | debug('info', 'Asserted queue: ' + queue.name); 193 | return queue; 194 | }); 195 | } 196 | 197 | 198 | /** 199 | * Creates an exchange. 200 | * @param {string} key 201 | * @param {string} name 202 | * @param {string} type 203 | * @param {Object=} opt_options 204 | * @return {Promise} 205 | */ 206 | createExchange(key, name, type, opt_options) { 207 | if (!key) 208 | return Promise.reject(new Error('You cannot create exchange without key.')); 209 | 210 | if (this.exchanges_[key]) 211 | return Promise.reject(new Error('You cannot create exchange with same key more than once.')); 212 | 213 | const exchange = new Exchange({ 214 | channel: this.channel, 215 | name: name, 216 | type: type, 217 | options: opt_options, 218 | rpc: this.rpc_ 219 | }); 220 | 221 | exchange.on('log', (...args) => this.emit('log', ...args)); 222 | 223 | return exchange.init() 224 | .then((exchange) => { 225 | this.exchanges_[key] = exchange; 226 | debug('info', 'Asserted exchange: ' + exchange.name); 227 | return exchange; 228 | }); 229 | } 230 | } 231 | 232 | 233 | /** 234 | * Default options. 235 | * @type {Object} 236 | */ 237 | AmqpKit.prototype.defaults = { 238 | id: 'microservice-default-id', 239 | rpc: true, 240 | connectionOptions: {} 241 | }; 242 | 243 | 244 | module.exports = AmqpKit; 245 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = require('./microservicekit'); 4 | module.exports.AmqpKit = require('./amqpkit'); 5 | module.exports.ShutdownKit = require('./shutdownkit'); 6 | module.exports.ErrorType = require('./lib/errors'); 7 | -------------------------------------------------------------------------------- /src/lib/errors/clienterror.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtendableError = require('./extendableerror'); 4 | 5 | 6 | class ClientError extends ExtendableError { 7 | constructor(m, p) { 8 | super(m, p); 9 | } 10 | } 11 | 12 | 13 | module.exports = ClientError; 14 | -------------------------------------------------------------------------------- /src/lib/errors/error-utils.js: -------------------------------------------------------------------------------- 1 | 2 | if (!('toJSON' in Error.prototype)) 3 | Object.defineProperty(Error.prototype, 'toJSON', { 4 | value: function () { 5 | var alt = {}; 6 | 7 | Object.getOwnPropertyNames(this).forEach(function (key) { 8 | alt[key] = this[key]; 9 | }, this); 10 | 11 | return alt; 12 | }, 13 | configurable: true, 14 | writable: true 15 | }); 16 | -------------------------------------------------------------------------------- /src/lib/errors/extendableerror.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | class ExtendableError extends Error { 5 | constructor(message, payload) { 6 | super(message, payload); 7 | this.name = this.constructor.name; 8 | this.message = message; 9 | this.payload = payload; 10 | Error.captureStackTrace(this, this.constructor.name) 11 | } 12 | } 13 | 14 | 15 | ExtendableError.prototype.toJSON = function() { 16 | return { 17 | message: this.message, 18 | payload: this.payload, 19 | name: this.name 20 | } 21 | }; 22 | 23 | 24 | module.exports = ExtendableError; 25 | -------------------------------------------------------------------------------- /src/lib/errors/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('./error-utils'); 4 | 5 | module.exports = { 6 | InternalError: require('./internalerror'), 7 | ClientError: require('./clienterror') 8 | }; 9 | -------------------------------------------------------------------------------- /src/lib/errors/internalerror.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const ExtendableError = require('./extendableerror'); 4 | 5 | 6 | class InternalError extends ExtendableError { 7 | constructor(m, p) { 8 | super(m, p); 9 | } 10 | } 11 | 12 | 13 | module.exports = InternalError; 14 | -------------------------------------------------------------------------------- /src/lib/event-emitter-extra/index.js: -------------------------------------------------------------------------------- 1 | 2 | const isArray = require('lodash/isArray'); 3 | const isFunction = require('lodash/isFunction'); 4 | const isNumber = require('lodash/isNumber'); 5 | const isRegExp = require('lodash/isRegExp'); 6 | const isString = require('lodash/isString'); 7 | const Listener = require('./listener'); 8 | 9 | 10 | class EventEmitterExtra { 11 | constructor() { 12 | this.maxListeners_ = EventEmitterExtra.defaultMaxListeners; 13 | this.maxRegexListeners_ = EventEmitterExtra.defaultMaxRegexListeners; 14 | this.listeners_ = []; 15 | this.regexListeners_ = []; 16 | this.eventListeners_ = {}; 17 | } 18 | 19 | 20 | addListener(eventName, handler, opt_execLimit, opt_prepend) { 21 | if (isArray(eventName) || isArray(handler)) { 22 | const events = isArray(eventName) ? eventName : [eventName]; 23 | const handlers = isArray(handler) ? handler : [handler]; 24 | events.forEach(event => { 25 | handlers.forEach(handler => { 26 | this.addListener(event, handler, opt_execLimit); 27 | }); 28 | }); 29 | return this; 30 | } 31 | 32 | const listener = new Listener(eventName, handler, opt_execLimit); 33 | 34 | if (listener.eventName) { 35 | if (!this.eventListeners_[listener.eventName]) 36 | this.eventListeners_[listener.eventName] = []; 37 | 38 | if (this.eventListeners_[listener.eventName].length >= this.maxListeners_) 39 | throw new Error(`Max listener count reached for event: ${eventName}`); 40 | 41 | this.emit('newListener', eventName, handler); 42 | 43 | if (opt_prepend) 44 | this.eventListeners_[listener.eventName].unshift(listener); 45 | else 46 | this.eventListeners_[listener.eventName].push(listener); 47 | } else if (listener.eventNameRegex) { 48 | if (this.regexListeners_.length >= this.maxRegexListeners_) 49 | throw new Error(`Max regex listener count reached`); 50 | 51 | this.emit('newListener', eventName, handler); 52 | 53 | if (opt_prepend) 54 | this.regexListeners_.unshift(listener); 55 | else 56 | this.regexListeners_.push(listener); 57 | } 58 | 59 | listener.onExpire = this.removeListener_.bind(this); 60 | this.listeners_.push(listener); 61 | 62 | return this; 63 | } 64 | 65 | 66 | prependListener(eventName, handler, opt_execLimit) { 67 | return this.addListener(eventName, handler, opt_execLimit, true); 68 | } 69 | 70 | 71 | prependOnceListener(eventName, handler) { 72 | return this.addListener(eventName, handler, 1, true); 73 | } 74 | 75 | 76 | prependManyListener(eventName, count, handler) { 77 | return this.addListener(eventName, handler, count, true); 78 | } 79 | 80 | 81 | removeListener_(listener) { 82 | remove(this.listeners_, listener); 83 | 84 | if (listener.eventName && isArray(this.eventListeners_[listener.eventName])) { 85 | remove(this.eventListeners_[listener.eventName], listener); 86 | 87 | if (this.eventListeners_[listener.eventName].length == 0) 88 | delete this.eventListeners_[listener.eventName]; 89 | } else if (listener.eventNameRegex) { 90 | remove(this.regexListeners_, listener); 91 | } 92 | 93 | this.emit('removeListener', listener.eventName || listener.eventNameRegex, listener.handler); 94 | } 95 | 96 | 97 | removeAllListeners(eventName) { 98 | if (isArray(eventName)) { 99 | eventName.forEach(event => this.removeAllListeners(event)); 100 | } else if (isString(eventName) && isArray(this.eventListeners_[eventName])) { 101 | const listeners = this.eventListeners_[eventName].slice(); 102 | listeners.forEach(listener => { 103 | this.removeListener_(listener); 104 | }); 105 | } else if (isRegExp(eventName)) { 106 | const regex = eventName; 107 | const listeners = this.regexListeners_.filter(listener => regexEquals(listener.eventNameRegex, regex)); 108 | listeners.forEach(listener => this.removeListener_(listener)); 109 | } else if (eventName == undefined) { 110 | this.removeAllListeners(this.eventNames()); 111 | this.removeAllListeners(this.regexes()); 112 | } 113 | 114 | return this; 115 | } 116 | 117 | 118 | removeListener(eventName, handler) { 119 | if (isArray(eventName) || isArray(handler)) { 120 | const events = isArray(eventName) ? eventName : [eventName]; 121 | const handlers = isArray(handler) ? handler : [handler]; 122 | events.forEach(event => { 123 | handlers.forEach(handler => { 124 | this.removeListener(event, handler); 125 | }); 126 | }); 127 | } else if (isString(eventName) && isArray(this.eventListeners_[eventName])) { 128 | const listeners = this.eventListeners_[eventName].filter(listener => listener.handler == handler); 129 | listeners.forEach(listener => this.removeListener_(listener)); 130 | } else if (isRegExp(eventName)) { 131 | const regex = eventName; 132 | const listeners = this.regexListeners_.filter( 133 | listener => 134 | regexEquals(listener.eventNameRegex, regex) && 135 | listener.handler == handler 136 | ); 137 | listeners.forEach(listener => this.removeListener_(listener)); 138 | } else { 139 | throw new Error('Event name should be string or regex.'); 140 | } 141 | 142 | return this; 143 | } 144 | 145 | 146 | eventNames() { 147 | return Object.keys(this.eventListeners_); 148 | } 149 | 150 | 151 | regexes() { 152 | return this.regexListeners_.map(listener => listener.eventNameRegex); 153 | } 154 | 155 | 156 | getMaxListeners() { 157 | return this.maxListeners_; 158 | } 159 | 160 | 161 | setMaxListeners(n) { 162 | if (!isNumber(n) || parseInt(n, 10) != n) 163 | throw new Error('n must be integer'); 164 | 165 | this.maxListeners_ = n; 166 | return this; 167 | } 168 | 169 | 170 | getMaxRegexListeners() { 171 | return this.maxRegexListeners_; 172 | } 173 | 174 | 175 | setMaxRegexListeners(n) { 176 | if (!isNumber(n) || parseInt(n, 10) != n) 177 | throw new Error('n must be integer'); 178 | 179 | this.maxRegexListeners_ = n; 180 | return this; 181 | } 182 | 183 | 184 | listenerCount(eventName) { 185 | // TODO: Support arrays 186 | if (isString(eventName)) { 187 | if (!this.eventListeners_[eventName]) 188 | return 0; 189 | 190 | return this.eventListeners_[eventName].length; 191 | } else if (isRegExp(eventName)) { 192 | return this.regexListeners_ 193 | .filter(listener => regexEquals(eventName, listener.eventNameRegex)) 194 | .length; 195 | } else { 196 | throw new Error('Event name should be string or regex.'); 197 | } 198 | } 199 | 200 | 201 | listeners(eventName) { 202 | // TODO: Support arrays 203 | if (isString(eventName)) { 204 | if (!this.eventListeners_[eventName]) 205 | return []; 206 | 207 | return this.eventListeners_[eventName].map(listener => listener.handler); 208 | } else if (isRegExp(eventName)) { 209 | return this.regexListeners_ 210 | .filter(listener => regexEquals(eventName, listener.eventNameRegex)) 211 | .map(listener => listener.handler); 212 | } else { 213 | throw new Error('Event name should be string or regex.'); 214 | } 215 | } 216 | 217 | 218 | on(eventName, handler) { 219 | return this.addListener(eventName, handler); 220 | } 221 | 222 | 223 | once(eventName, handler) { 224 | return this.addListener(eventName, handler, 1); 225 | } 226 | 227 | 228 | many(eventName, count, handler) { 229 | return this.addListener(eventName, handler, count); 230 | } 231 | 232 | 233 | emit(eventName, ...args) { 234 | if (isArray(eventName)) { 235 | let rv = []; 236 | eventName.forEach(event => { 237 | const results = this.emit(event, ...args); 238 | rv = rv.concat(results); 239 | }); 240 | return rv; 241 | } else if (!isString(eventName)) { 242 | throw new Error('Event name should be string'); 243 | } 244 | 245 | let results = []; 246 | const event = {name: eventName}; 247 | 248 | if (this.eventListeners_[eventName]) { 249 | const nameMatchedResults = this.eventListeners_[eventName] 250 | .slice() // Shallow copy for not to skip if listener is expired 251 | .map(listener => listener.execute( 252 | Object.assign({}, listener, {event}), 253 | args 254 | )); 255 | results = results.concat(nameMatchedResults); 256 | } 257 | 258 | const regexMatchedResults = this.regexListeners_ 259 | .filter(listener => listener.testRegexWith(eventName)) 260 | .map(listener => listener.execute( 261 | Object.assign({}, listener, {event}), 262 | args 263 | )); 264 | 265 | results = results.concat(regexMatchedResults); 266 | 267 | return results.length > 0 ? results : false; 268 | } 269 | 270 | 271 | emitAsync(...args) { 272 | const rv = this.emit(...args); 273 | 274 | if (!rv) 275 | return Promise.resolve(); 276 | 277 | return Promise.all(rv); 278 | } 279 | } 280 | 281 | 282 | EventEmitterExtra.defaultMaxListeners = 10; 283 | EventEmitterExtra.defaultMaxRegexListeners = 10; 284 | EventEmitterExtra.Listener = Listener; 285 | 286 | 287 | function regexEquals(a, b) { 288 | /* istanbul ignore if */ 289 | if (typeof a !== 'object' || typeof b !== 'object') return false; 290 | return a.toString() === b.toString(); 291 | } 292 | 293 | 294 | function remove(arr, predicate) { 295 | let removedItems = []; 296 | 297 | /* istanbul ignore if */ 298 | if (isFunction(predicate)) { 299 | removedItems = arr.filter(predicate); 300 | } else if (arr.indexOf(predicate) > -1) { 301 | removedItems.push(predicate); 302 | } 303 | 304 | removedItems.forEach(item => { 305 | const index = arr.indexOf(item); 306 | arr.splice(index, 1); 307 | }); 308 | 309 | return removedItems; 310 | } 311 | 312 | 313 | module.exports = EventEmitterExtra; 314 | -------------------------------------------------------------------------------- /src/lib/event-emitter-extra/listener.js: -------------------------------------------------------------------------------- 1 | const isString = require('lodash/isString'); 2 | const isRegExp = require('lodash/isRegExp'); 3 | const isFunction = require('lodash/isFunction'); 4 | const isNumber = require('lodash/isNumber'); 5 | 6 | 7 | class Listener { 8 | constructor(eventName, handler, execLimit = 0) { 9 | if (isString(eventName)) { 10 | this.eventName = eventName; 11 | } else if (isRegExp(eventName)) { 12 | this.eventNameRegex = eventName; 13 | } else { 14 | throw new Error('Event name to be listened should be string or regex'); 15 | } 16 | 17 | if (!isFunction(handler)) 18 | throw new Error('Handler should be a function'); 19 | 20 | if (!isNumber(execLimit) || parseInt(execLimit, 10) != execLimit) 21 | throw new Error('Execute limit should be integer'); 22 | 23 | this.handler = handler; 24 | this.execCount = 0; 25 | this.execLimit = execLimit; 26 | } 27 | 28 | 29 | execute(that, args) { 30 | const rv = this.handler.apply(that, args); 31 | this.execCount++; 32 | 33 | if (this.execLimit && this.execCount >= this.execLimit) { 34 | this.onExpire(this); 35 | } 36 | 37 | return rv; 38 | } 39 | 40 | 41 | testRegexWith(eventName) { 42 | const regex = this.eventNameRegex; 43 | return regex.test(eventName); 44 | } 45 | 46 | 47 | onExpire() { 48 | 49 | } 50 | } 51 | 52 | 53 | module.exports = Listener; 54 | -------------------------------------------------------------------------------- /src/lib/exchange.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const debug = require('debug')('microservice-kit:lib:exchange'); 4 | const async = require('async-q'); 5 | const _ = require('lodash'); 6 | const EventEmitterExtra = require('./event-emitter-extra'); 7 | const uuid = require('uuid'); 8 | const Message = require('./message'); 9 | const Response = require('./response'); 10 | 11 | 12 | 13 | class Exchange extends EventEmitterExtra { 14 | constructor(options) { 15 | super(); 16 | 17 | if (!options.channel) 18 | throw new Error('MicroserviceKit: Queue cannot be ' + 19 | 'constructed without a channel'); 20 | 21 | this.channel = options.channel; 22 | this.name = options.name || ''; 23 | this.key = options.key || this.name; 24 | this.type = options.type || 'direct'; 25 | this.options = options.options || {}; 26 | this.rpc_ = options.rpc; 27 | this.callbacks_ = {}; 28 | } 29 | 30 | 31 | /** 32 | * Init exhange 33 | */ 34 | init() { 35 | return this.channel 36 | .assertExchange(this.name, this.type, this.options) 37 | .then((exchange) => { 38 | this.exchange_ = exchange; 39 | return this; 40 | }); 41 | } 42 | 43 | 44 | /** 45 | * Publishes an event on this exchange. Its just implements callback (rpc) 46 | * support 47 | * @param {string} routingKey 48 | * @param {string} eventName 49 | * @param {Object=} opt_payload 50 | * @param {Object=} opt_options 51 | * @return {Promise} 52 | */ 53 | publishEvent(routingKey, eventName, opt_payload, opt_options) { 54 | if (!_.isString(eventName)) 55 | return Promise.reject(new Error('Cannot publish. Event name is required.')); 56 | 57 | const message = new Message(eventName, opt_payload); 58 | const options = _.assign({}, this.publishDefaults, opt_options || {}); 59 | const content = new Buffer(JSON.stringify(message.toJSON() || {})); 60 | 61 | if (!this.rpc_ || options.dontExpectRpc) { 62 | this.log_('debug', 'Publishing event', { 63 | eventName, 64 | routingKey, 65 | exchange: this.key 66 | }); 67 | 68 | return Promise.resolve(this.channel.publish(this.name, routingKey, content, options)); 69 | } 70 | 71 | options.correlationId = uuid.v4(); 72 | options.replyTo = this.rpc_.getUniqueQueueName(); 73 | 74 | if (_.isNumber(options.timeout) && options.timeout > 0) { 75 | options.expiration = options.timeout.toString(); 76 | } 77 | 78 | const rv = new Promise((resolve, reject) => { 79 | this.log_('debug', 'Publishing event', { 80 | eventName, 81 | routingKey, 82 | correlationId: options.correlationId, 83 | exchange: this.key 84 | }); 85 | 86 | this.channel.publish(this.name, routingKey, content, options); 87 | this.rpc_.registerCallback(options.correlationId, {reject, resolve}, options.timeout); 88 | }); 89 | 90 | rv.progress = (callback) => { 91 | let rpcCb_ = this.rpc_.getCallback(options.correlationId); 92 | if (rpcCb_) 93 | rpcCb_.progress = callback; 94 | 95 | return rv; 96 | }; 97 | 98 | return rv; 99 | } 100 | 101 | 102 | /** 103 | * Log methods. It uses debug module but also custom logger method if exists. 104 | */ 105 | log_(...args) { 106 | debug(...args); 107 | this.emit('log', ...args); 108 | } 109 | } 110 | 111 | 112 | /** 113 | * Default publish & sendToQueue options. 114 | * @type {Object} 115 | */ 116 | Exchange.prototype.publishDefaults = Exchange.publishDefaults = { 117 | dontExpectRpc: false, 118 | timeout: 30 * 1000, 119 | persistent: true 120 | }; 121 | 122 | 123 | module.exports = Exchange; 124 | -------------------------------------------------------------------------------- /src/lib/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | 6 | class Message { 7 | /** 8 | * This is microservicekit's message entity. This class is wrapper of normal 9 | * rabbitmq's message content. Implements event names for in-microservice-routing. 10 | * @param {string} eventName Name of the event. 11 | * @param {Object=} opt_payload Optional additional dta. 12 | */ 13 | constructor(eventName, opt_payload) { 14 | this.eventName = eventName; 15 | this.payload = _.assign({}, opt_payload || {}); 16 | } 17 | 18 | 19 | /** 20 | * Returns json of object. 21 | * @return {Object} 22 | */ 23 | toJSON() { 24 | return { 25 | eventName: this.eventName, 26 | payload: this.payload 27 | }; 28 | } 29 | 30 | 31 | /** 32 | * Parses rabbitmq's native message object and returns new message. 33 | * @static 34 | * @param {Object} msg 35 | * @return {Message} 36 | */ 37 | static parseMessage(msg) { 38 | const rawMessage = JSON.parse(msg.content.toString()); 39 | return Message.parse(rawMessage); 40 | } 41 | 42 | 43 | /** 44 | * Parses raw (json) object and returns new message. 45 | * @param {Object} raw 46 | * @return {Message} 47 | */ 48 | static parse(raw) { 49 | return new Message(raw.eventName, raw.payload); 50 | } 51 | } 52 | 53 | 54 | module.exports = Message; 55 | -------------------------------------------------------------------------------- /src/lib/queue.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const debug = require('debug')('microservice-kit:lib:queue'); 4 | const async = require('async-q'); 5 | const EventEmitterExtra = require('./event-emitter-extra'); 6 | const uuid = require('uuid'); 7 | const _ = require('lodash'); 8 | const Message = require('./message'); 9 | const Exchange = require('./exchange'); 10 | const Response = require('./response'); 11 | const Router = require('./router'); 12 | 13 | 14 | class Queue extends EventEmitterExtra { 15 | constructor(options) { 16 | super(); 17 | if (!options.channel) 18 | throw new Error('MicroserviceKit: Queue cannot be ' + 19 | 'constructed without a channel'); 20 | 21 | this.channel = options.channel; 22 | this.name = options.name || ''; 23 | this.rpc_ = options.rpc; 24 | this.options = options.options || {}; 25 | this.tracer = options.tracer; 26 | } 27 | 28 | 29 | /** 30 | * Init queue 31 | */ 32 | init() { 33 | return this.channel 34 | .assertQueue(this.name, this.options) 35 | .then((queue) => { 36 | this.queue_ = queue; 37 | return this; 38 | }); 39 | } 40 | 41 | 42 | consumeRaw_(consumeCallback, options) { 43 | this.channel.consume(this.getUniqueName(), consumeCallback, options || {}); 44 | } 45 | 46 | 47 | /** 48 | * Consumes all the messages on the queue. 49 | * @param {Function} callback 50 | * @param {Object=} opt_options 51 | */ 52 | consume_(callback, opt_options) { 53 | const options = _.assign({}, this.consumeDefaults, opt_options || {}); 54 | this.consumer_ = callback; 55 | 56 | return this.channel.consume(this.getUniqueName(), (msg) => { 57 | try { 58 | const data = JSON.parse(msg.content.toString()); 59 | 60 | const message = Message.parse(data); 61 | const receivedAt = new Date(); 62 | 63 | this.log_('debug', 'Received event', { 64 | correlationId: msg.properties.correlationId, 65 | eventName: message.eventName 66 | }); 67 | 68 | const done = (err, data) => { 69 | const duration = new Date() - receivedAt; 70 | const logPayload = { 71 | duration, 72 | eventName: message.eventName 73 | }; 74 | 75 | if (msg.properties.replyTo && msg.properties.correlationId) { 76 | const response = new Response(err, data, true); 77 | this.channel.sendToQueue( 78 | msg.properties.replyTo, 79 | new Buffer(JSON.stringify(response.toJSON())), 80 | {correlationId: msg.properties.correlationId} 81 | ); 82 | 83 | logPayload.correlationId = msg.properties.correlationId; 84 | logPayload.response = response; 85 | } 86 | 87 | logPayload.labels = { 88 | duration, 89 | eventName: logPayload.eventName 90 | }; 91 | 92 | let logLevel = 'debug'; 93 | 94 | if (err) { 95 | logLevel = 'error'; 96 | logPayload.error = err.toJSON(); 97 | } 98 | 99 | this.log_(logLevel, 'Consumed event', _.omit(logPayload, 'response')); 100 | this.emit('consumedEvent', logPayload); 101 | 102 | if (!options.noAck) 103 | this.channel.ack(msg); 104 | }; 105 | 106 | const progress = (data) => { 107 | if (msg.properties.replyTo && msg.properties.correlationId) { 108 | const response = new Response(null, data, false); 109 | this.channel.sendToQueue( 110 | msg.properties.replyTo, 111 | new Buffer(JSON.stringify(response.toJSON())), 112 | {correlationId: msg.properties.correlationId} 113 | ); 114 | } 115 | } 116 | 117 | const routingKey = msg.fields.routingKey; 118 | 119 | this.consumer_ && this.consumer_(data, done, progress, routingKey); 120 | } catch(err) { 121 | this.log_('error', 'Error while consuming message', {err, content: msg.content}); 122 | 123 | if (!options.noAck) { 124 | this.log_('warn', 'Negative acknowledging...'); 125 | this.channel.nack(msg); 126 | } 127 | } 128 | }, options); 129 | } 130 | 131 | 132 | /** 133 | * Consumes just matched events in the queue. 134 | * @param {string} eventName 135 | * @param {Function} callback 136 | * @param {Object=} opt_options 137 | */ 138 | consumeEvent(eventName, callback, opt_options) { 139 | if (!this.consumer_) { 140 | this.router = new Router(this.getUniqueName()); 141 | this.consume_(this.router.handle.bind(this.router), opt_options); 142 | } 143 | 144 | this.router.register(eventName, callback); 145 | } 146 | 147 | 148 | /** 149 | * Binds this queue to an exchange over a pattern. 150 | * @param {string} exchange 151 | * @param {string} pattern 152 | * @returns {Promise} 153 | */ 154 | bind(exchange, pattern) { 155 | return this.channel.bindQueue(this.getUniqueName(), exchange, pattern); 156 | } 157 | 158 | 159 | /** 160 | * Un-binds this queue to an exchange over a pattern. 161 | * @param {string} exchange 162 | * @param {string} pattern 163 | * @returns {Promise} 164 | */ 165 | unbind(exchange, pattern) { 166 | return this.channel.unbindQueue(this.getUniqueName(), exchange, pattern); 167 | } 168 | 169 | 170 | /** 171 | * Returns real queue name on rabbitmq. 172 | * @return {string} 173 | */ 174 | getUniqueName() { 175 | return this.queue_.queue; 176 | } 177 | 178 | 179 | /** 180 | * Sends an event to queue on main channel. Its just implements callback (rpc) 181 | * support. 182 | * @param {string} eventName 183 | * @param {Object=} opt_payload 184 | * @param {Object=} opt_options 185 | * @return {Promise} 186 | */ 187 | sendEvent(eventName, opt_payload, opt_options) { 188 | if (!_.isString(eventName)) 189 | return Promise.reject(new Error('Cannot send event to queue. Event name is required.')); 190 | 191 | const message = new Message(eventName, opt_payload); 192 | const queue = this.getUniqueName(); 193 | const options = _.assign({}, Exchange.publishDefaults, opt_options || {}); 194 | const content = new Buffer(JSON.stringify(message.toJSON() || {})); 195 | 196 | if (!this.rpc_ || options.dontExpectRpc) { 197 | this.log_('debug', 'Sending event to queue', { 198 | eventName, 199 | target: this.name || this.getUniqueName() 200 | }); 201 | 202 | return Promise.resolve(this.channel.sendToQueue(queue, content, options)); 203 | } 204 | 205 | options.correlationId = uuid.v4(); 206 | options.replyTo = this.rpc_.getUniqueQueueName(); 207 | 208 | if (_.isNumber(options.timeout) && options.timeout > 0) { 209 | options.expiration = options.timeout.toString(); 210 | } 211 | 212 | const rv = new Promise((originalResolve, originalReject) => { 213 | let span; 214 | if (this.tracer) { 215 | span = this.tracer.createChildSpan({name: `amqpkit-sendEvent:${eventName}`}); 216 | span.addLabel('eventName', eventName); 217 | } 218 | 219 | this.log_('debug', 'Sending event to queue', { 220 | eventName, 221 | correlationId: options.correlationId, 222 | target: this.name || this.getUniqueName(), 223 | 224 | }); 225 | 226 | function resolve(result) { 227 | if (span) { 228 | span.addLabel('status', 'successful'); 229 | span.endSpan(); 230 | } 231 | originalResolve(result); 232 | } 233 | function reject(err) { 234 | if (span) { 235 | span.addLabel('status', 'failed'); 236 | span.endSpan(); 237 | } 238 | originalReject(err); 239 | } 240 | 241 | const callbacks = {resolve, reject}; 242 | if (this.tracer) { 243 | callbacks.resolve = this.tracer.wrap(resolve); 244 | callbacks.reject = this.tracer.wrap(reject); 245 | } 246 | 247 | this.channel.sendToQueue(queue, content, options); 248 | this.rpc_.registerCallback(options.correlationId, callbacks, options.timeout); 249 | }); 250 | 251 | rv.progress = (callback) => { 252 | let rpcCb_ = this.rpc_.getCallback(options.correlationId); 253 | if (rpcCb_) 254 | rpcCb_.progress = callback; 255 | 256 | return rv; 257 | }; 258 | 259 | return rv; 260 | } 261 | 262 | 263 | /** 264 | * Log methods. It uses debug module but also custom logger method if exists. 265 | */ 266 | log_(...args) { 267 | debug(...args); 268 | this.emit('log', ...args); 269 | } 270 | } 271 | 272 | 273 | /** 274 | * Default consume options. 275 | * @type {Object} 276 | */ 277 | Queue.prototype.consumeDefaults = { 278 | noAck: false 279 | }; 280 | 281 | 282 | module.exports = Queue; 283 | -------------------------------------------------------------------------------- /src/lib/response.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const Errors = require('./errors'); 5 | 6 | 7 | 8 | class Response { 9 | /** 10 | * This is microservicekit's response entitiy. This class is used for RPC protocol. 11 | * @param {*=} opt_err Error object if exists. 12 | * @param {Object=} opt_payload Optional additional data. 13 | * @param {boolean=} opt_done Whether the job is completed or not. By setting this value to false, you can send progress events! 14 | */ 15 | constructor(opt_err, opt_payload, opt_done) { 16 | this.err = opt_err; 17 | this.payload = opt_payload; 18 | this.done = _.isBoolean(opt_done) ? opt_done : true; 19 | } 20 | 21 | 22 | /** 23 | * Returns json of object. 24 | * @return {Object} 25 | */ 26 | toJSON() { 27 | let err = this.err; 28 | 29 | if (_.isObject(err) && err instanceof Error && err.name == 'Error') 30 | err = {message: err.message, name: 'Error'}; 31 | 32 | return { 33 | err, 34 | payload: this.payload, 35 | done: this.done 36 | }; 37 | } 38 | 39 | 40 | /** 41 | * Parses rabbitmq's native message object and returns new response. 42 | * @static 43 | * @param {Object} msg 44 | * @return {Message} 45 | */ 46 | static parseMessage(msg) { 47 | const rawMessage = JSON.parse(msg.content.toString()); 48 | return Response.parse(rawMessage); 49 | } 50 | 51 | 52 | /** 53 | * Parses raw (json) object and returns new response. 54 | * @param {Object} raw 55 | * @return {Message} 56 | */ 57 | static parse(raw) { 58 | let err = raw.err; 59 | 60 | if (_.isObject(err) && err.name) { 61 | switch (err.name) { 62 | case 'Error': 63 | err = new Error(err.message); 64 | break; 65 | case 'InternalError': 66 | err = new Errors.InternalError(err.message, err.payload); 67 | break; 68 | case 'ClientError': 69 | err = new Errors.ClientError(err.message, err.payload); 70 | break; 71 | } 72 | } 73 | 74 | return new Response(err, raw.payload, raw.done); 75 | } 76 | } 77 | 78 | 79 | module.exports = Response; 80 | -------------------------------------------------------------------------------- /src/lib/router.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const debug = require('debug')('microservice-kit:amqpkit:router'); 4 | const Message = require('./message'); 5 | 6 | 7 | 8 | /** 9 | * Routes all the messsages incoming from queue by its event name. This routing just works 10 | * inside a microservice. 11 | * TODO: Implement unregister and wildcard event handling. 12 | */ 13 | class Router { 14 | constructor() { 15 | this.callbacks_ = {}; 16 | } 17 | 18 | 19 | /** 20 | * Registers to given event. You can not register the same event more than once in the same queue. 21 | * If you do the previous bindings will be forgetten. 22 | * @param {string} eventName 23 | * @param {Function} callback 24 | */ 25 | register(eventName, callback) { 26 | this.callbacks_[eventName] = callback; 27 | } 28 | 29 | 30 | /** 31 | * Handles incoming message from queue. 32 | * @param {Object} data 33 | * @param {Function} done 34 | * @param {Function} progress 35 | * @param {string} routingKey 36 | */ 37 | handle(data, done, progress, routingKey) { 38 | debug('Incoming message:' + JSON.stringify(data)); 39 | 40 | const message = Message.parse(data); 41 | const callback = this.callbacks_[message.eventName]; 42 | 43 | if (callback) 44 | callback(message.payload, done, progress, routingKey) 45 | else 46 | debug('Unhandled message:' + JSON.stringify(data)); 47 | } 48 | } 49 | 50 | 51 | module.exports = Router; 52 | -------------------------------------------------------------------------------- /src/lib/rpc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | const EventEmitterExtra = require('./event-emitter-extra'); 4 | const debug = require('debug')('microservice-kit:lib:rpc'); 5 | const _ = require('lodash'); 6 | const Response = require('./response'); 7 | const Queue = require('./queue'); 8 | 9 | 10 | 11 | class RPC extends EventEmitterExtra { 12 | constructor(opt_options) { 13 | super(); 14 | 15 | this.initialized = false; 16 | this.queue_ = null; 17 | this.channel_ = null; 18 | this.callbacks_ = {}; 19 | this.timeouts_ = {}; 20 | this.registerDates_ = {}; 21 | } 22 | 23 | 24 | /** 25 | * Init rpc manager. 26 | */ 27 | init(connection, opt_queueName) { 28 | debug('Initializing rpc channel.'); 29 | return connection 30 | .createChannel() 31 | .then((channel) => { 32 | debug('rpc channel initialized.'); 33 | 34 | this.channel_ = channel; 35 | this.queue_ = new Queue({ 36 | name: opt_queueName, 37 | options: { 38 | /* We are effectively creating an ephemeral queue here for RPC. 39 | * exclusive: true makes sure queue is deleted when connection is closed. 40 | * durable: false makes sure queue is not written to disk. It will be deleted when connection is dropped anyway. 41 | * autoDelete: true is here for verbosity. It basically deletes the queue when all connections are dropped. 42 | * Check out: https://amqp-node.github.io/amqplib/channel_api.html#channel_assertQueue 43 | */ 44 | exclusive: true, 45 | durable: false, 46 | autoDelete: true, 47 | }, 48 | channel: this.channel_ 49 | }); 50 | 51 | debug('Initializing rpc queue.'); 52 | return this.queue_.init(); 53 | }) 54 | .then(() => { 55 | debug('rpc queue initialized.'); 56 | debug('Consuming rpc queue...'); 57 | return this.queue_.consumeRaw_(this.consumer.bind(this), {noAck: true}); 58 | }) 59 | .then(() => { 60 | debug('rpc initialized.'); 61 | this.initialized = true; 62 | }); 63 | } 64 | 65 | 66 | 67 | /** 68 | * Handles messages coming from rpc queue. 69 | * @param {Object} msg 70 | */ 71 | consumer(msg) { 72 | const correlationId = msg.properties.correlationId; 73 | 74 | if (!this.initialized || !correlationId || !this.callbacks_[correlationId]) 75 | return; 76 | 77 | const callbacks = this.callbacks_[correlationId]; 78 | 79 | try { 80 | const response = Response.parseMessage(msg); 81 | 82 | if (!response.done) { 83 | callbacks.progress && callbacks.progress(response.payload); 84 | return; 85 | } 86 | 87 | if (this.registerDates_[correlationId]) { 88 | const duration = new Date() - this.registerDates_[correlationId]; 89 | this.log_('debug', 'Got response', {correlationId, duration}); 90 | delete this.registerDates_[correlationId]; 91 | } 92 | 93 | if (response.err) 94 | callbacks.reject(response.err); 95 | else 96 | callbacks.resolve(response.payload); 97 | 98 | if (this.timeouts_[correlationId]) { 99 | clearTimeout(this.timeouts_[correlationId]); 100 | delete this.timeouts_[correlationId]; 101 | } 102 | 103 | delete this.callbacks_[correlationId]; 104 | } catch(err) { 105 | this.log_('error', 'Cannot consume rpc message, probably json parse error.', {msg, err}); 106 | } 107 | } 108 | 109 | getUniqueQueueName() { 110 | return this.queue_.getUniqueName(); 111 | } 112 | 113 | registerCallback(key, funcs, opt_timeout) { 114 | this.callbacks_[key] = funcs; 115 | this.registerDates_[key] = new Date(); 116 | 117 | if (_.isNumber(opt_timeout) && opt_timeout > 0) { 118 | this.timeouts_[key] = setTimeout(() => { 119 | const callbacks = this.callbacks_[key]; 120 | callbacks && callbacks.reject && callbacks.reject(new Error('Timeout exceed.')); 121 | this.log_('error', 'Timeout exceed', {correlationId: key}); 122 | delete this.callbacks_[key]; 123 | delete this.timeouts_[key]; 124 | delete this.registerDates_[key]; 125 | }, opt_timeout) 126 | } 127 | } 128 | 129 | getCallback(key) { 130 | return this.callbacks_[key]; 131 | } 132 | 133 | 134 | /** 135 | * Log methods. It uses debug module but also custom logger method if exists. 136 | */ 137 | log_(...args) { 138 | debug(...args); 139 | this.emit('log', ...args); 140 | } 141 | } 142 | 143 | 144 | 145 | module.exports = RPC; 146 | -------------------------------------------------------------------------------- /src/microservicekit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const fs = require('fs'); 5 | const EventEmitterExtra = require('./lib/event-emitter-extra'); 6 | const uuid = require('uuid'); 7 | const debug = require('debug')('microservice-kit:microservicekit'); 8 | const Chance = require('chance'); 9 | 10 | const AmqpKit = require('./amqpkit'); 11 | const ShutdownKit = require('./shutdownkit'); 12 | 13 | 14 | class MicroserviceKit extends EventEmitterExtra { 15 | constructor(opt_options) { 16 | super(); 17 | 18 | this.options_ = _.assign({}, this.defaults, opt_options || {}); 19 | this.id = new Chance().first().toLowerCase() + '-' + uuid.v4().split('-')[0]; 20 | this.amqpKit = null; 21 | this.shutdownKit = ShutdownKit; 22 | 23 | this.shutdownKit.on('log', (...args) => { 24 | this.emit('shutdownKitLog', ...args); 25 | args.splice(1, 0, '[shutdownkit]'); 26 | this.emit('log', ...args); 27 | }); 28 | } 29 | 30 | 31 | init() { 32 | if (!this.options_.amqp) 33 | return Promise.resolve(); 34 | 35 | const amqpOptions = _.assign({}, this.options_.amqp, {id: this.getName()}); 36 | this.amqpKit = new AmqpKit(amqpOptions); 37 | 38 | this.amqpKit.on('log', (...args) => { 39 | this.emit('amqpKitLog', ...args); 40 | args.splice(1, 0, '[amqpkit]'); 41 | this.emit('log', ...args); 42 | }); 43 | 44 | this.amqpKit.on('consumedEvent', payload => this.emit('consumedEvent', payload)); 45 | 46 | return this.amqpKit.init(); 47 | } 48 | 49 | 50 | getName() { 51 | return this.options_.type + '-' + this.id; 52 | } 53 | } 54 | 55 | 56 | MicroserviceKit.prototype.defaults = { 57 | type: 'microservice', 58 | amqp: {} 59 | }; 60 | 61 | 62 | module.exports = MicroserviceKit; 63 | -------------------------------------------------------------------------------- /src/shutdownkit.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const EventEmitterExtra = require('./lib/event-emitter-extra'); 4 | const debug = require('debug')('microservice-kit:shutdownkit'); 5 | const _ = require('lodash'); 6 | const async = require('async'); 7 | 8 | 9 | class ShutdownKit extends EventEmitterExtra { 10 | constructor() { 11 | super(); 12 | // Force resume node process! 13 | process.stdin.resume(); 14 | this.jobs_ = []; 15 | this.bindEvents_(); 16 | this.isShuttingDown = false; 17 | } 18 | 19 | 20 | /** 21 | * Add a job to graceful shutdown process. 22 | * @param {Function} job Function of job. Do not forget to call done function! 23 | */ 24 | addJob(job) { 25 | this.jobs_.push(job); 26 | } 27 | 28 | 29 | /** 30 | * Binds common termination signals. 31 | */ 32 | bindEvents_() { 33 | process.on('uncaughtException', this.onUncaughtException_.bind(this)); 34 | process.on('SIGTERM', this.onSigTerm_.bind(this)); 35 | process.on('SIGINT', this.onSigInt_.bind(this)); 36 | } 37 | 38 | 39 | /** 40 | * On uncaught exception. 41 | * @param {Error} err 42 | */ 43 | onUncaughtException_(err) { 44 | this.log_('error', 'Uncaught Exception received!', err); 45 | this.gracefulShutdown(); 46 | } 47 | 48 | 49 | /** 50 | * On SIGTERM 51 | */ 52 | onSigTerm_() { 53 | this.log_('info', 'SIGTERM received!'); 54 | this.gracefulShutdown(); 55 | } 56 | 57 | 58 | /** 59 | * On SIGINT 60 | */ 61 | onSigInt_() { 62 | this.log_('info', 'SIGINT received!'); 63 | this.gracefulShutdown(); 64 | } 65 | 66 | 67 | /** 68 | * Tries to do all the jobs before shutdown. 69 | */ 70 | gracefulShutdown() { 71 | // TODO: Add a timeout maybe? 72 | if (this.isShuttingDown) return; 73 | this.isShuttingDown = true; 74 | this.log_('info', 'Trying to shutdown gracefully...'); 75 | async.series(this.jobs_.reverse(), (err) => { 76 | if (err) { 77 | this.log_('error', 'Some jobs failed', err); 78 | this.log_('info', 'Quiting anyway...'); 79 | } 80 | else 81 | this.log_('info', 'All jobs done, quiting...'); 82 | 83 | this.exit_(); 84 | }); 85 | } 86 | 87 | 88 | /** 89 | * Exists current process. 90 | */ 91 | exit_() { 92 | this.log_('info', 'Bye!'); 93 | process.exit(); 94 | } 95 | 96 | 97 | /** 98 | * Log methods. It uses debug module but also custom logger method if exists. 99 | */ 100 | log_(...args) { 101 | debug(...args); 102 | this.emit('log', ...args); 103 | } 104 | } 105 | 106 | 107 | // Singleton 108 | if (!global.shutdownKit_) 109 | global.shutdownKit_ = new ShutdownKit(); 110 | 111 | module.exports = global.shutdownKit_; 112 | -------------------------------------------------------------------------------- /test/amqpkit-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const should = chai.should(); 9 | chai.use(sinonChai.default); 10 | chai.use(chaiAsPromised.default); 11 | 12 | 13 | const amqp = require('amqplib'); 14 | const ShutdownKit = require('../src/shutdownkit'); 15 | const AMQPKit = require('../src/amqpkit'); 16 | const ChannelStubs = require('./lib/channel-stubs'); 17 | const ConnectionStubs = require('./lib/connection-stubs'); 18 | 19 | 20 | describe('AMQPKit', function() { 21 | describe('#init without rpc', function() { 22 | beforeEach(function() { 23 | this.options = { 24 | url: 'localhost', 25 | rpc: false, 26 | queues: [ 27 | {name: 'q1', key: 'q1', options: {durable: true}}, 28 | {name: 'q2', key: 'q2', options: {durable: true}}, 29 | {name: 'q3', key: 'q3', options: {durable: true}} 30 | ], 31 | exchanges: [ 32 | {name: 'e1', key: 'e1', type: 'direct'}, 33 | {name: 'e2', key: 'e2', type: 'direct'}, 34 | {name: 'e3', key: 'e3', type: 'direct'} 35 | ] 36 | }; 37 | 38 | this.connectStub = sinon.stub(amqp, 'connect') 39 | .returns(Promise.resolve(ConnectionStubs.generate())); 40 | 41 | this.shutdownKitStub = sinon.stub(ShutdownKit, 'addJob'); 42 | this.amqpKit = new AMQPKit(this.options); 43 | 44 | sinon.spy(this.amqpKit, 'createQueue'); 45 | sinon.spy(this.amqpKit, 'createExchange'); 46 | 47 | return this.amqpKit.init(); 48 | }); 49 | 50 | it('connect should be called with url', function() { 51 | this.connectStub.should.calledWith(this.options.url); 52 | }); 53 | 54 | it('without rpc one channel should be created', function() { 55 | this.amqpKit.connection.createChannel.calledOnce; 56 | }); 57 | 58 | it('connect should be called once', function() { 59 | this.connectStub.should.calledOnce; 60 | }); 61 | 62 | it('should add shutdown job', function() { 63 | this.shutdownKitStub.should.calledOnce; 64 | }); 65 | 66 | it('createQueue should be called 3 times', function() { 67 | this.amqpKit.createQueue.should.calledThrice; 68 | }); 69 | 70 | it('createExchange should be called 3 times', function() { 71 | this.amqpKit.createExchange.should.calledThrice; 72 | }); 73 | 74 | afterEach(function() { 75 | this.connectStub.restore(); 76 | this.shutdownKitStub.restore(); 77 | }) 78 | }); 79 | describe('#init with rpc', function() { 80 | beforeEach(function() { 81 | this.options = { 82 | url: 'localhost', 83 | rpc: true, 84 | queues: [ 85 | {name: 'q1', key: 'q1', options: {durable: true}}, 86 | {name: 'q2', key: 'q2', options: {durable: true}}, 87 | {name: 'q3', key: 'q3', options: {durable: true}} 88 | ], 89 | exchanges: [ 90 | {name: 'e1', key: 'e1', type: 'direct'}, 91 | {name: 'e2', key: 'e2', type: 'direct'}, 92 | {name: 'e3', key: 'e3', type: 'direct'} 93 | ] 94 | }; 95 | 96 | this.connectStub = sinon.stub(amqp, 'connect') 97 | .returns(Promise.resolve(ConnectionStubs.generate())); 98 | 99 | this.shutdownKitStub = sinon.stub(ShutdownKit, 'addJob'); 100 | 101 | this.amqpKit = new AMQPKit(this.options); 102 | 103 | sinon.spy(this.amqpKit, 'createQueue'); 104 | sinon.spy(this.amqpKit, 'createExchange'); 105 | 106 | return this.amqpKit.init(); 107 | }); 108 | 109 | it('connect should be called with url', function() { 110 | this.connectStub.should.calledWith(this.options.url); 111 | }); 112 | 113 | it('with rpc one channel should be created', function() { 114 | this.amqpKit.connection.createChannel.calledTwice; 115 | }); 116 | 117 | it('connect should be called once', function() { 118 | this.connectStub.should.calledOnce; 119 | }); 120 | 121 | it('should add shutdown job', function() { 122 | this.shutdownKitStub.should.calledOnce; 123 | }); 124 | 125 | it('createQueue should be called 3 times', function() { 126 | this.amqpKit.createQueue.should.calledThrice; 127 | }); 128 | 129 | it('createExchange should be called 3 times', function() { 130 | this.amqpKit.createExchange.should.calledThrice; 131 | }); 132 | 133 | afterEach(function() { 134 | this.connectStub.restore(); 135 | this.shutdownKitStub.restore(); 136 | }) 137 | }); 138 | 139 | }); 140 | -------------------------------------------------------------------------------- /test/exchange-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const should = chai.should(); 8 | chai.use(sinonChai.default); 9 | chai.use(chaiAsPromised.default); 10 | 11 | const Exchange = require('../src/lib/exchange'); 12 | const ChannelStubs = require('./lib/channel-stubs'); 13 | const RPCStubs = require('./lib/rpc-stubs'); 14 | 15 | 16 | 17 | describe('Exchange', function() { 18 | describe('#init', function() { 19 | it('exchange should be asserted with proper name and options', function () { 20 | const channelStub = ChannelStubs.generate(); 21 | const exchangeOptions = { 22 | channel: channelStub, 23 | name: 'test-exchange', 24 | key: 'test', 25 | type: 'direct', 26 | options: {} 27 | }; 28 | const exchange = new Exchange(exchangeOptions); 29 | return exchange 30 | .init() 31 | .then(() => { 32 | channelStub.assertExchange.should.calledWith(exchangeOptions.name, exchangeOptions.type, exchangeOptions.options); 33 | channelStub.assertExchange.should.calledOnce; 34 | }); 35 | }); 36 | 37 | it('exchange can be asserted without a name', function () { 38 | const channelStub = ChannelStubs.generate(); 39 | const exchangeOptions = { 40 | channel: channelStub, 41 | key: 'test', 42 | type: 'direct', 43 | options: {} 44 | }; 45 | const exchange = new Exchange(exchangeOptions); 46 | return exchange 47 | .init() 48 | .then(() => { 49 | exchange.name.should.equal(''); 50 | }); 51 | }); 52 | 53 | 54 | it('should resolve exchange itself', function () { 55 | const channelStub = ChannelStubs.generate(); 56 | const exchangeOptions = { 57 | channel: channelStub 58 | }; 59 | const exchange = new Exchange(exchangeOptions); 60 | return exchange 61 | .init() 62 | .should.eventually.equal(exchange); 63 | }); 64 | 65 | it('should set key as name if not provided', function () { 66 | const channelStub = ChannelStubs.generate(); 67 | const exchangeOptions = { 68 | channel: channelStub, 69 | name: 'test' 70 | }; 71 | const exchange = new Exchange(exchangeOptions); 72 | return exchange 73 | .init() 74 | .then(() => { 75 | exchange.key.should.equal('test'); 76 | }); 77 | }); 78 | }); 79 | 80 | 81 | describe('#publishEvent', function() { 82 | describe('without rpc', function() { 83 | beforeEach(function() { 84 | const channelStub = ChannelStubs.generate(); 85 | const exchangeOptions = { 86 | channel: channelStub, 87 | name: 'test-exchange', 88 | options: {} 89 | }; 90 | this.exchange = new Exchange(exchangeOptions); 91 | return this.exchange.init() 92 | }) 93 | 94 | it('publishEvent fails without eventName 1', function () { 95 | return this.exchange.publishEvent().should.eventually.rejected; 96 | }); 97 | 98 | it('publishEvent fails without eventName 2', function () { 99 | return this.exchange.publishEvent('routing-key', null, {foo: 'bar'}).should.eventually.rejected; 100 | }); 101 | 102 | it('publishEvent does not fails without routing key', function () { 103 | return this.exchange.publishEvent(null, 'event', {foo: 'bar'}).should.eventually.fulfilled; 104 | }); 105 | }) 106 | 107 | describe('with rpc', function() { 108 | beforeEach(function() { 109 | const channelStub = ChannelStubs.generate(); 110 | const rpcStub = RPCStubs.generate(); 111 | const exchangeOptions = { 112 | channel: channelStub, 113 | name: 'test-exchange', 114 | options: {}, 115 | rpc: rpcStub 116 | }; 117 | this.exchange = new Exchange(exchangeOptions); 118 | return this.exchange.init() 119 | }) 120 | 121 | it('publishEvent fails without eventName 1', function () { 122 | return this.exchange.publishEvent().should.eventually.rejected; 123 | }); 124 | 125 | it('publishEvent fails without eventName 2', function () { 126 | return this.exchange.publishEvent(null, {foo: 'bar'}).should.eventually.rejected; 127 | }); 128 | 129 | it('rpc.getUniqueQueueName() should be called', function () { 130 | this.exchange.publishEvent(null, 'event', {foo: 'bar'}); 131 | return this.exchange.rpc_.getUniqueQueueName.should.be.calledOnce; 132 | }); 133 | 134 | it('rpc.getUniqueQueueName() should not be called if dontExpectRpc', function () { 135 | this.exchange.publishEvent(null, 'event', {foo: 'bar'}, {dontExpectRpc: true}); 136 | return this.exchange.rpc_.getUniqueQueueName.should.not.be.called; 137 | }); 138 | 139 | it('publishEvent should resolve after ack', function () { 140 | const thenSpy = sinon.spy(); 141 | return this.exchange.publishEvent(null, 'event', {foo: 'bar'}) 142 | .then(thenSpy) 143 | .then(() => { 144 | thenSpy.should.be.calledOnce; 145 | }); 146 | 147 | }); 148 | 149 | it('progress should be called if provided', function () { 150 | const progressSpy = sinon.spy(); 151 | return this.exchange.publishEvent(null, 'event', {foo: 'bar'}) 152 | .progress(progressSpy) 153 | .then(() => { 154 | this.exchange.rpc_.getCallback.should.be.calledOnce; 155 | progressSpy.should.be.calledOnce; 156 | }); 157 | 158 | }); 159 | 160 | it('rpc.registerCallback should be called if provided', function () { 161 | return this.exchange.publishEvent(null, 'event', {foo: 'bar'}) 162 | .then(() => { 163 | this.exchange.rpc_.registerCallback.should.be.calledOnce; 164 | }); 165 | 166 | }); 167 | 168 | it('progress should be undefined if dontExpectRpc', function () { 169 | chai.expect(this.exchange.publishEvent(null, 'event', {foo: 'bar'}, {dontExpectRpc: true}).progress).to.be.an('undefined'); 170 | }); 171 | 172 | it('rpc.getCallback() should not be called without progress handler', function () { 173 | return this.exchange.publishEvent(null, 'event', {foo: 'bar'}) 174 | .then(() => { 175 | this.exchange.rpc_.getCallback.should.not.be.called; 176 | }); 177 | }); 178 | 179 | it('rpc.getCallback() should be called with a progress handler', function () { 180 | return this.exchange.publishEvent(null, 'event', {foo: 'bar'}) 181 | .progress(() => { 182 | this.exchange.rpc_.getCallback.should.be.called; 183 | }); 184 | }); 185 | }) 186 | }) 187 | 188 | 189 | }); 190 | -------------------------------------------------------------------------------- /test/lib/channel-stubs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const Message = require('./mocks/message'); 5 | 6 | exports.generate = function(opt_message) { 7 | const message = opt_message || Message.mock(); 8 | return { 9 | assertQueue: sinon.stub().returns(Promise.resolve({ 10 | queue: 'test-queue' 11 | })), 12 | assertExchange: sinon.spy((name, type, options) => Promise.resolve({name, type, options})), 13 | bindQueue: sinon.stub(), 14 | unbindQueue: sinon.stub(), 15 | consume: sinon.stub().yields(message), 16 | sendToQueue: sinon.stub(), 17 | ack: sinon.stub(), 18 | nack: sinon.stub(), 19 | publish: sinon.spy((name, routingKey, content, options) => Promise.resolve()) 20 | }; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /test/lib/connection-stubs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | const ChannelStub = require('./channel-stubs'); 5 | 6 | 7 | exports.generate = function(opt_message) { 8 | return { 9 | createChannel: sinon.spy(() => Promise.resolve(ChannelStub.generate())), 10 | on: sinon.spy() 11 | }; 12 | } 13 | 14 | -------------------------------------------------------------------------------- /test/lib/mocks/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const Message = require('../../../src/lib/message'); 4 | 5 | 6 | exports.mock = function() { 7 | const message = new Message('test-event', {test: 'payload'}); 8 | 9 | return { 10 | fields: { 11 | routingKey: '' 12 | }, 13 | properties: { 14 | correlationId: '123', 15 | replyTo: 'abc' 16 | }, 17 | content: { 18 | toString: () => JSON.stringify(message.toJSON()), 19 | } 20 | }; 21 | } 22 | 23 | -------------------------------------------------------------------------------- /test/lib/rpc-stubs.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const sinon = require('sinon'); 4 | 5 | exports.generate = function(opt_message) { 6 | const callbacks = {}; 7 | return { 8 | getUniqueQueueName: sinon.stub().returns('rpc-queue'), 9 | registerCallback: sinon.spy((id, cb, opts) => { 10 | callbacks[id] = cb; 11 | setTimeout(() => callbacks[id].progress && callbacks[id].progress(), 50); 12 | setTimeout(() => callbacks[id].resolve(), 100); 13 | }), 14 | getCallback: sinon.spy((id) => { 15 | return callbacks[id]; 16 | }) 17 | }; 18 | } 19 | 20 | -------------------------------------------------------------------------------- /test/message-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const should = chai.should(); 9 | chai.use(sinonChai.default); 10 | chai.use(chaiAsPromised.default); 11 | 12 | const Message = require('../src/lib/message'); 13 | const ErrorTypes = require('../src/lib/errors'); 14 | 15 | 16 | describe('Message', function() { 17 | describe('#parse', function() { 18 | it('should parse eventName', function() { 19 | const eventName = 'event1'; 20 | const raw = {eventName}; 21 | 22 | const message = Message.parse(raw); 23 | message.eventName.should.equal(eventName); 24 | }) 25 | 26 | it('should parse payload', function() { 27 | const payload = {foo: 'bar'}; 28 | const raw = {payload}; 29 | 30 | const message = Message.parse(raw); 31 | message.payload.should.deep.equal(payload); 32 | }) 33 | 34 | it('should set payload to empty object if not provided', function() { 35 | const raw = {}; 36 | const message = Message.parse(raw); 37 | message.payload.should.be.an('object'); 38 | }) 39 | }); 40 | }); 41 | -------------------------------------------------------------------------------- /test/queue-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const chai = require('chai'); 4 | const sinon = require('sinon'); 5 | const sinonChai = require('sinon-chai'); 6 | const chaiAsPromised = require('chai-as-promised'); 7 | const should = chai.should(); 8 | chai.use(sinonChai.default); 9 | chai.use(chaiAsPromised.default); 10 | 11 | const Queue = require('../src/lib/queue'); 12 | const ChannelStubs = require('./lib/channel-stubs'); 13 | const RPCStubs = require('./lib/rpc-stubs'); 14 | 15 | 16 | 17 | describe('Queue', function() { 18 | describe('#init', function() { 19 | it('queue should be asserted with proper name and options', function () { 20 | const channelStub = ChannelStubs.generate(); 21 | const queueOptions = { 22 | channel: channelStub, 23 | name: 'test-queue', 24 | options: { 25 | exclusive: true 26 | } 27 | }; 28 | const queue = new Queue(queueOptions); 29 | return queue 30 | .init() 31 | .then(() => { 32 | channelStub.assertQueue.should.calledWith(queueOptions.name, queueOptions.options); 33 | channelStub.assertQueue.should.calledOnce; 34 | }); 35 | }); 36 | 37 | it('queue can be asserted without a name', function () { 38 | const channelStub = ChannelStubs.generate(); 39 | const queueOptions = { 40 | channel: channelStub, 41 | options: { 42 | exclusive: true 43 | } 44 | }; 45 | const queue = new Queue(queueOptions); 46 | return queue 47 | .init() 48 | .then(() => { 49 | channelStub.assertQueue.should.calledWith('', queueOptions.options); 50 | channelStub.assertQueue.should.calledOnce; 51 | }); 52 | }); 53 | 54 | it('queue can be asserted without options', function () { 55 | const channelStub = ChannelStubs.generate(); 56 | const queueOptions = { 57 | channel: channelStub, 58 | name: 'test-queue' 59 | }; 60 | const queue = new Queue(queueOptions); 61 | return queue 62 | .init() 63 | .then(() => { 64 | channelStub.assertQueue.should.calledWith('test-queue', {}); 65 | channelStub.assertQueue.should.calledOnce; 66 | }); 67 | }); 68 | 69 | it('init throws error after assertion fails', function () { 70 | const channelStub = ChannelStubs.generate(); 71 | channelStub.assertQueue = sinon.stub().returns(Promise.reject(new Error('some error'))); 72 | const queueOptions = { 73 | channel: channelStub, 74 | name: 'test-queue' 75 | }; 76 | const queue = new Queue(queueOptions); 77 | return queue 78 | .init() 79 | .should.have.eventually.rejected; 80 | }); 81 | 82 | 83 | }) 84 | 85 | 86 | 87 | describe('#consume methods', function() { 88 | 89 | beforeEach(function() { 90 | const channelStub = ChannelStubs.generate(); 91 | const queueOptions = { 92 | channel: channelStub, 93 | name: 'test-queue', 94 | options: { 95 | exclusive: true 96 | } 97 | }; 98 | this.queue = new Queue(queueOptions); 99 | return this.queue.init() 100 | }) 101 | 102 | describe('#consume_', function() { 103 | it('channel consume should be called with callback and options', function () { 104 | const callback = sinon.spy(); 105 | const consumeOptions = { noAck: false }; 106 | this.queue.consume_(callback, consumeOptions); 107 | this.queue.channel.consume.should.calledOnce; 108 | }); 109 | }) 110 | 111 | describe('#consumeRaw_', function() { 112 | it('channel consume should be called with callback and options', function () { 113 | const callback = sinon.spy(); 114 | const consumeOptions = { noAck: false }; 115 | this.queue.consumeRaw_(callback, consumeOptions); 116 | this.queue.channel.consume.should.calledWith('test-queue', callback, consumeOptions); 117 | this.queue.channel.consume.should.calledOnce; 118 | }); 119 | }) 120 | 121 | 122 | describe('#consumeEvent', function() { 123 | it('channel consume should be when consumeEvent is called at the first place', function () { 124 | this.queue.consumeEvent('test-event', () => {}); 125 | this.queue.channel.consume.should.called; 126 | }); 127 | 128 | it('channel consume should be called only once and at the first consumeEvent', function () { 129 | this.queue.consumeEvent('test-event', () => {}); 130 | this.queue.consumeEvent('test-event2', () => {}); 131 | this.queue.channel.consume.should.calledOnce; 132 | }); 133 | 134 | it('router.register should be called', function () { 135 | // router is undefined before the first consumeEvent 136 | // therefore, register method can not be spied. 137 | this.queue.consumeEvent('test-event', () => {}); 138 | 139 | sinon.spy(this.queue.router, 'register'); 140 | this.queue.consumeEvent('test-event2', () => {}); 141 | this.queue.router.register.should.calledOnce; 142 | }); 143 | }) 144 | 145 | describe('#bind', function() { 146 | it('bindQueue should be called', function () { 147 | this.queue.bind('exchange', 'pattern'); 148 | this.queue.channel.bindQueue.should.calledWith(this.queue.getUniqueName(), 'exchange', 'pattern'); 149 | this.queue.channel.bindQueue.should.calledOnce; 150 | }); 151 | }) 152 | 153 | describe('#unbind', function() { 154 | it('unbindQueue should be called', function () { 155 | this.queue.unbind('exchange', 'pattern'); 156 | this.queue.channel.unbindQueue.should.calledWith(this.queue.getUniqueName(), 'exchange', 'pattern'); 157 | this.queue.channel.unbindQueue.should.calledOnce; 158 | }); 159 | }) 160 | 161 | describe('#sendEvent', function() { 162 | 163 | 164 | it('sendEvent fails without eventName 2', function () { 165 | return this.queue.sendEvent(null, {foo: 'bar'}).should.eventually.rejected; 166 | }); 167 | }) 168 | }) 169 | 170 | describe('#sendEvents', function() { 171 | describe('without rpc', function() { 172 | beforeEach(function() { 173 | const channelStub = ChannelStubs.generate(); 174 | const queueOptions = { 175 | channel: channelStub, 176 | name: 'test-queue', 177 | options: { 178 | exclusive: true 179 | } 180 | }; 181 | this.queue = new Queue(queueOptions); 182 | return this.queue.init() 183 | }) 184 | 185 | it('sendEvent fails without eventName 1', function () { 186 | return this.queue.sendEvent().should.eventually.rejected; 187 | }); 188 | 189 | it('sendEvent fails without eventName 2', function () { 190 | return this.queue.sendEvent(null, {foo: 'bar'}).should.eventually.rejected; 191 | }); 192 | }) 193 | 194 | describe('with rpc', function() { 195 | beforeEach(function() { 196 | const channelStub = ChannelStubs.generate(); 197 | const rpcStub = RPCStubs.generate(); 198 | const queueOptions = { 199 | channel: channelStub, 200 | name: 'test-queue', 201 | options: { 202 | exclusive: true 203 | }, 204 | rpc: rpcStub 205 | }; 206 | this.queue = new Queue(queueOptions); 207 | return this.queue.init() 208 | }) 209 | 210 | it('sendEvent fails without eventName 1', function () { 211 | return this.queue.sendEvent().should.eventually.rejected; 212 | }); 213 | 214 | it('sendEvent fails without eventName 2', function () { 215 | return this.queue.sendEvent(null, {foo: 'bar'}).should.eventually.rejected; 216 | }); 217 | 218 | it('rpc.getUniqueQueueName() should be called', function () { 219 | this.queue.sendEvent('event', {foo: 'bar'}); 220 | return this.queue.rpc_.getUniqueQueueName.should.be.calledOnce; 221 | }); 222 | 223 | it('rpc.getUniqueQueueName() should not be called if dontExpectRpc', function () { 224 | this.queue.sendEvent('event', {foo: 'bar'}, {dontExpectRpc: true}); 225 | return this.queue.rpc_.getUniqueQueueName.should.not.be.called; 226 | }); 227 | 228 | it('sendEvent should resolve after ack', function () { 229 | const thenSpy = sinon.spy(); 230 | return this.queue.sendEvent('event', {foo: 'bar'}) 231 | .then(thenSpy) 232 | .then(() => { 233 | thenSpy.should.be.calledOnce; 234 | }); 235 | 236 | }); 237 | 238 | it('progress should be called if provided', function () { 239 | const progressSpy = sinon.spy(); 240 | return this.queue.sendEvent('event', {foo: 'bar'}) 241 | .progress(progressSpy) 242 | .then(() => { 243 | this.queue.rpc_.getCallback.should.be.calledOnce; 244 | progressSpy.should.be.calledOnce; 245 | }); 246 | 247 | }); 248 | 249 | it('rpc.registerCallback should be called if provided', function () { 250 | return this.queue.sendEvent('event', {foo: 'bar'}) 251 | .then(() => { 252 | this.queue.rpc_.registerCallback.should.be.calledOnce; 253 | }); 254 | 255 | }); 256 | 257 | it('progress should be undefined if dontExpectRpc', function () { 258 | chai.expect(this.queue.sendEvent('event', {foo: 'bar'}, {dontExpectRpc: true}).progress).to.be.an('undefined'); 259 | }); 260 | 261 | it('rpc.getCallback() should not be called without progress handler', function () { 262 | return this.queue.sendEvent('event', {foo: 'bar'}) 263 | .then(() => { 264 | this.queue.rpc_.getCallback.should.not.be.called; 265 | }); 266 | }); 267 | 268 | it('rpc.getCallback() should be called with a progress handler', function () { 269 | return this.queue.sendEvent('event', {foo: 'bar'}) 270 | .progress(() => { 271 | this.queue.rpc_.getCallback.should.be.called; 272 | }); 273 | }); 274 | }) 275 | }) 276 | 277 | 278 | }); 279 | -------------------------------------------------------------------------------- /test/response-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const should = chai.should(); 9 | chai.use(sinonChai.default); 10 | chai.use(chaiAsPromised.default); 11 | 12 | const Response = require('../src/lib/response'); 13 | const ErrorTypes = require('../src/lib/errors'); 14 | 15 | 16 | describe('Response', function() { 17 | describe('#parse', function() { 18 | it('should parse native error properly', function() { 19 | const payload = {foo: 'bar'}; 20 | const err = new Error('internal error'); 21 | const done = false; 22 | const raw = {err: {name: err.name, message: err.message}, payload, done}; 23 | 24 | const response = Response.parse(raw); 25 | response.err.should.instanceOf(Error); 26 | response.err.message.should.equal(err.message); 27 | }) 28 | 29 | it('should parse internal error properly', function() { 30 | const payload = {foo: 'bar'}; 31 | const err = new ErrorTypes.InternalError('internal error'); 32 | const done = false; 33 | const raw = {err: err.toJSON(), payload, done}; 34 | 35 | const response = Response.parse(raw); 36 | response.err.should.instanceOf(ErrorTypes.InternalError); 37 | response.err.message.should.equal(err.message); 38 | }) 39 | 40 | it('should parse client error properly', function() { 41 | const payload = {foo: 'bar'}; 42 | const err = new ErrorTypes.ClientError('client error'); 43 | const done = false; 44 | const raw = {err: err.toJSON(), payload, done}; 45 | 46 | const response = Response.parse(raw); 47 | response.err.should.instanceOf(ErrorTypes.ClientError); 48 | response.err.message.should.equal(err.message); 49 | }) 50 | 51 | it('should parse done properly', function() { 52 | const done = false; 53 | const raw = {done}; 54 | const response = Response.parse(raw); 55 | response.done.should.equal(done); 56 | }) 57 | 58 | it('should parse done as true if not provided', function() { 59 | const raw = {}; 60 | const response = Response.parse(raw); 61 | response.done.should.equal(true); 62 | }) 63 | 64 | it('should parse payload properly', function() { 65 | const payload = {foo: 'bar'}; 66 | const raw = {payload}; 67 | const response = Response.parse(raw); 68 | response.payload.should.equal(payload); 69 | }) 70 | }); 71 | }); 72 | -------------------------------------------------------------------------------- /test/router-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const should = chai.should(); 9 | chai.use(sinonChai.default); 10 | chai.use(chaiAsPromised.default); 11 | 12 | const Router = require('../src/lib/router'); 13 | const Message = require('../src/lib/message'); 14 | 15 | 16 | describe('Router', function() { 17 | beforeEach(function() { 18 | this.router = new Router(); 19 | sinon.spy(this.router, 'register'); 20 | sinon.spy(this.router, 'handle'); 21 | }); 22 | 23 | it('should store handler in memory', function() { 24 | const handler = function() { }; 25 | this.router.register('event', handler); 26 | chai.expect(this.router.callbacks_['event']).to.equal(handler); 27 | }); 28 | 29 | it('second register should override handler in memory', function() { 30 | const handler1 = function() { }; 31 | const handler2 = function() { }; 32 | this.router.register('event', handler1); 33 | this.router.register('event', handler2); 34 | chai.expect(this.router.callbacks_['event']).not.to.equal(handler1); 35 | chai.expect(this.router.callbacks_['event']).to.equal(handler2); 36 | }); 37 | 38 | it('handle method should route events properly', function() { 39 | const payload = {foo: 'bar'}; 40 | const spy1 = sinon.spy(); 41 | const spy2 = sinon.spy(); 42 | this.router.register('event1', spy1); 43 | this.router.register('event2', spy2); 44 | 45 | const done = function() {}; 46 | const progress = function() {}; 47 | const routingKey = 'routing-key'; 48 | this.router.handle({eventName: 'event1', payload: payload}, done, progress, routingKey); 49 | this.router.handle({eventName: 'event2', payload: payload}, done, progress, routingKey); 50 | 51 | spy1.should.calledOnce; 52 | spy1.should.calledWith(payload, done, progress, routingKey); 53 | spy2.should.calledOnce; 54 | spy2.should.calledWith(payload, done, progress, routingKey); 55 | }); 56 | 57 | it('handle method should route multiple events', function() { 58 | const payload = {foo: 'bar'}; 59 | const spy = sinon.spy(); 60 | this.router.register('event', spy); 61 | 62 | _.times(10, () => { 63 | this.router.handle({eventName: 'event', payload: payload}); 64 | }) 65 | 66 | spy.should.callCount(10); 67 | spy.should.calledWith(payload); 68 | }); 69 | }); 70 | -------------------------------------------------------------------------------- /test/rpc-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | const chai = require('chai'); 5 | const sinon = require('sinon'); 6 | const sinonChai = require('sinon-chai'); 7 | const chaiAsPromised = require('chai-as-promised'); 8 | const should = chai.should(); 9 | chai.use(sinonChai.default); 10 | chai.use(chaiAsPromised.default); 11 | 12 | const ErrorTypes = require('../src/lib/errors'); 13 | const RPC = require('../src/lib/rpc'); 14 | const ConnectionStubs = require('./lib/connection-stubs'); 15 | const Message = require('./lib/mocks/message'); 16 | 17 | 18 | describe('RPC', function() { 19 | beforeEach(function() { 20 | this.rpc = new RPC(); 21 | sinon.spy(this.rpc, 'init'); 22 | sinon.spy(this.rpc, 'consumer'); 23 | sinon.spy(this.rpc, 'getUniqueQueueName'); 24 | sinon.spy(this.rpc, 'registerCallback'); 25 | sinon.spy(this.rpc, 'getCallback'); 26 | 27 | this.ConnectionStub = ConnectionStubs.generate(); 28 | }); 29 | 30 | describe('#init', function() { 31 | it('should call createChannel', function() { 32 | return this.rpc 33 | .init(this.ConnectionStub) 34 | .then(() => { 35 | this.ConnectionStub.createChannel.should.calledOnce; 36 | }); 37 | }) 38 | 39 | it('initialized must be true', function() { 40 | return this.rpc 41 | .init(this.ConnectionStub) 42 | .then(() => { 43 | chai.expect(this.rpc.initialized).to.equal(true); 44 | }); 45 | }) 46 | 47 | it('channel consume should be called', function() { 48 | return this.rpc 49 | .init(this.ConnectionStub) 50 | .then(() => { 51 | this.rpc.channel_.consume.should.calledOnce; 52 | chai.expect(this.rpc.queue_).not.to.equal(undefined) 53 | }); 54 | }) 55 | }) 56 | 57 | describe('#consumer', function() { 58 | beforeEach(function() { 59 | this.callbacks = { 60 | resolve: sinon.spy(), 61 | reject: sinon.spy(), 62 | progress: sinon.spy() 63 | }; 64 | 65 | return this.rpc 66 | .init(this.ConnectionStub) 67 | .then(() => this.rpc.registerCallback('id1', this.callbacks)); 68 | }) 69 | 70 | it('should do nothing if there is no correlationId', function() { 71 | const msg = Message.mock(); 72 | delete msg.properties.correlationId; 73 | this.rpc.consumer(msg); 74 | this.callbacks.resolve.should.not.called; 75 | this.callbacks.reject.should.not.called; 76 | this.callbacks.progress.should.not.called; 77 | }) 78 | 79 | it('should call resolve if there is a valid correlationId', function() { 80 | const msg = Message.mock(); 81 | msg.properties.correlationId = 'id1'; 82 | this.rpc.consumer(msg); 83 | this.callbacks.resolve.should.calledOnce; 84 | }) 85 | 86 | it('should reject if there is an error', function() { 87 | const msg = Message.mock(); 88 | const errMessage = 'Something wrong'; 89 | const errorObject = new ErrorTypes.InternalError(errMessage); 90 | msg.properties.correlationId = 'id1'; 91 | msg.content.toString = () => { 92 | return JSON.stringify({ 93 | err: errorObject 94 | }) 95 | }; 96 | this.rpc.consumer(msg); 97 | this.callbacks.reject.should.have.been.calledWithMatch(sinon.match({message: errMessage, name: 'InternalError'})); 98 | this.callbacks.resolve.should.not.called; 99 | }) 100 | 101 | it('should call progress, if done is explicitly defined as false', function() { 102 | const msg = Message.mock(); 103 | msg.properties.correlationId = 'id1'; 104 | msg.content.toString = () => { 105 | return JSON.stringify({ 106 | done: false, // !!!! 107 | payload: {foo: 'bar'}, 108 | eventName: 'event-name' 109 | }) 110 | }; 111 | this.rpc.consumer(msg); 112 | this.callbacks.progress.should.calledOnce; 113 | }) 114 | }) 115 | 116 | describe('#registerCallback', function() { 117 | beforeEach(function() { 118 | return this.rpc.init(this.ConnectionStub); 119 | }) 120 | 121 | it('should store callbacks in memory', function() { 122 | const callbacks = { 123 | resolve: sinon.spy(), 124 | reject: sinon.spy() 125 | }; 126 | this.rpc.registerCallback('id1', callbacks); 127 | chai.expect(this.rpc.getCallback('id1')).equal(callbacks); 128 | }); 129 | 130 | it('should remove callbacks after timeout', function(done) { 131 | const callbacks = { 132 | resolve: sinon.spy(), 133 | reject: sinon.spy() 134 | }; 135 | this.rpc.registerCallback('id1', callbacks, 10); 136 | setTimeout(() => { 137 | chai.expect(this.rpc.getCallback('id1')).equal(undefined); 138 | done(); 139 | }, 20); 140 | }); 141 | 142 | it('should not remove callbacks with negative timeout', function(done) { 143 | const callbacks = { 144 | resolve: sinon.spy(), 145 | reject: sinon.spy() 146 | }; 147 | this.rpc.registerCallback('id1', callbacks, -10); 148 | setTimeout(() => { 149 | chai.expect(this.rpc.getCallback('id1')).equal(callbacks); 150 | done(); 151 | }, 20); 152 | }); 153 | 154 | it('should reject after timeout', function(done) { 155 | const callbacks = { 156 | resolve: sinon.spy(), 157 | reject: sinon.spy() 158 | }; 159 | this.rpc.registerCallback('id1', callbacks, 10); 160 | setTimeout(() => { 161 | callbacks.reject.should.calledOnce; 162 | done(); 163 | }, 20); 164 | }); 165 | }) 166 | }); 167 | --------------------------------------------------------------------------------