├── .nvmrc ├── .npmrc ├── rabbit_enabled_plugins ├── .npmignore ├── demo ├── topic │ ├── subscriber.js │ ├── publisher-topic.js │ ├── subscriber-topic-left.js │ ├── subscriber-topic-right.js │ └── topology.js ├── pubsub │ ├── README.md │ ├── topology.js │ ├── publisher.js │ └── subscriber.js └── .eslintrc.js ├── src ├── defer.js ├── log.js ├── amqp │ ├── channel.js │ ├── exchange.js │ └── connection.js ├── info.js ├── publishLog.js ├── config.js ├── index.d.ts └── ackBatch.js ├── .gitignore ├── MAINTAINERS.md ├── CONTRIBUTORS.md ├── .editorconfig ├── spec ├── integration │ ├── configuration.js │ ├── connection.spec.js │ ├── noack.spec.js │ ├── typeless.spec.js │ ├── nobatch.spec.js │ ├── publish.spec.js │ ├── noReplyQueue.spec.js │ ├── fanout.spec.js │ ├── randomQueue.spec.js │ ├── addPassiveQueue.spec.js │ ├── mandatory.spec.js │ ├── directReplyQueue.spec.js │ ├── unrouted.spec.js │ ├── subscription.spec.js │ ├── typeSpecific.spec.js │ ├── poisonMessages.spec.js │ ├── wildCardTypes.spec.js │ ├── rejection.spec.js │ ├── badConnection.spec.js │ ├── topicExchange.spec.js │ ├── consistentHash.spec.js │ ├── bulkPublish.spec.js │ ├── unhandled.spec.js │ ├── queueSpecificHandle.spec.js │ ├── request.spec.js │ └── purgeQueue.spec.js ├── behavior │ ├── emitter.js │ ├── ssl │ │ └── ssl-connection.spec.js │ ├── publishLog.spec.js │ ├── configuration.spec.js │ ├── queue.spec.js │ ├── queueFsm.spec.js │ └── exchangeFsm.spec.js └── setup.js ├── .github └── workflows │ └── test-coverage-actions.yml ├── docs ├── logging.md ├── notwascally.md └── publishing.md ├── RESOURCES.md ├── LICENSE ├── ACKNOWLEDGEMENTS.md ├── package.json ├── CODE_OF_CONDUCT.md ├── HOW_TO_CONTRIBUTE.md └── README.md /.nvmrc: -------------------------------------------------------------------------------- 1 | 22 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | engine-strict=true -------------------------------------------------------------------------------- /rabbit_enabled_plugins: -------------------------------------------------------------------------------- 1 | [rabbitmq_consistent_hash_exchange,rabbitmq_management]. 2 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | demo/ 2 | tmp/ 3 | node_modules/ 4 | log/ 5 | spec/ 6 | plato/ 7 | coverage/ 8 | .vagrant/ 9 | .idea/ 10 | .nyc_output/ 11 | .editorconfig 12 | .travis.yml 13 | .nvmrc 14 | Dockerfile 15 | 16 | -------------------------------------------------------------------------------- /demo/topic/subscriber.js: -------------------------------------------------------------------------------- 1 | 2 | const rabbit = require('../../src/index.js'); 3 | 4 | require('./topology')(rabbit) 5 | .then(function () { 6 | require('./subscriber-topic-left')(rabbit); 7 | require('./subscriber-topic-right')(rabbit); 8 | }); 9 | -------------------------------------------------------------------------------- /src/defer.js: -------------------------------------------------------------------------------- 1 | function defer () { 2 | const deferred = { 3 | resolve: null, 4 | reject: null, 5 | promise: null 6 | }; 7 | deferred.promise = new Promise(function (resolve, reject) { 8 | deferred.reject = reject; 9 | deferred.resolve = resolve; 10 | }); 11 | return deferred; 12 | } 13 | 14 | module.exports = defer; 15 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Project Specific 2 | tmp/ 3 | plato/ 4 | 5 | # Logs 6 | log/ 7 | debug.log 8 | *npm-debug.log 9 | 10 | # Generated Output 11 | dist/ 12 | coverage/ 13 | .nyc_output/ 14 | node_modules/ 15 | 16 | # Editors 17 | *.swp 18 | *.swo 19 | .idea/ 20 | .vscode/ 21 | 22 | # System Files 23 | Thumbs.db 24 | .DS_Store 25 | 26 | # Vagrant 27 | .vagrant/ 28 | /Vagrantfile 29 | -------------------------------------------------------------------------------- /MAINTAINERS.md: -------------------------------------------------------------------------------- 1 | ## Maintainers List 2 | 3 | | Github Username | Name | Contact Info | 4 | | :------- | :-------------- | :------------------------ | 5 | | zlintz | Zach Lintz | lintz.zach@gmail.com | 6 | | auroq | Parker Johansen | johansen.parker@gmail.com | 7 | 8 | 9 | If you're interested in helping maintain Foo-Foo-MQ, contact us! 10 | -------------------------------------------------------------------------------- /CONTRIBUTORS.md: -------------------------------------------------------------------------------- 1 | ## Contributors 2 | 3 | These folks have all made significant contributions to the original [rabbot](https://github.com/arobson/rabbot) implementation on which this project is based: 4 | 5 | * [Alex Robson](http://github.com/arobson) 6 | * [Derick Bailey](http://derickbailey.com/) 7 | * [Doug Neiner](http://code.dougneiner.com) 8 | * [Brian Edgerton](https://github.com/brianedgerton) 9 | * [Jim Cowart](http://github.com/ifandelse) 10 | * [Mario Kozjak](https://github.com/mkozjak) 11 | * [Austin Young](http://github.com/LeankitAustin) 12 | * [John Mathis](http://github.com/JohnDMathis) 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs 2 | # editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | # spaces in JS unless otherwise specified 13 | [**.js] 14 | indent_style = space 15 | indent_size = 2 16 | 17 | [**.jsx] 18 | indent_style = space 19 | indent_size = 2 20 | 21 | [**.html] 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [**.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [**.md] 30 | indent_style = space 31 | indent_size = 2 32 | -------------------------------------------------------------------------------- /src/log.js: -------------------------------------------------------------------------------- 1 | // var log = require( "whistlepunk" ).log; 2 | const log = require('bole'); 3 | const debug = require('debug'); 4 | const debugEnv = process.env.DEBUG; 5 | 6 | const debugOut = { 7 | write: function (data) { 8 | const entry = JSON.parse(data); 9 | debug(entry.name)(entry.level, entry.message); 10 | } 11 | }; 12 | 13 | if (debugEnv) { 14 | log.output({ 15 | level: 'debug', 16 | stream: debugOut 17 | }); 18 | } 19 | 20 | module.exports = function (config) { 21 | if (typeof config === 'string') { 22 | return log(config); 23 | } else { 24 | log.output(config); 25 | return log; 26 | } 27 | }; 28 | -------------------------------------------------------------------------------- /spec/integration/configuration.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | connection: { 3 | protocol: 'amqp', 4 | name: 'default', 5 | user: 'guest', 6 | pass: 'guest', 7 | host: '127.0.0.1', 8 | port: 5672, 9 | vhost: '%2f', 10 | replyQueue: 'customReplyQueue' 11 | }, 12 | 13 | noReplyQueue: { 14 | name: 'noReplyQueue', 15 | user: 'guest', 16 | pass: 'guest', 17 | server: '127.0.0.1', 18 | port: 5672, 19 | vhost: '%2f', 20 | replyQueue: false 21 | }, 22 | 23 | directReplyQueue: { 24 | name: 'directReplyQueue', 25 | user: 'guest', 26 | pass: 'guest', 27 | server: '127.0.0.1', 28 | port: 5672, 29 | vhost: '%2f', 30 | replyQueue: 'rabbit' 31 | } 32 | }; 33 | -------------------------------------------------------------------------------- /.github/workflows/test-coverage-actions.yml: -------------------------------------------------------------------------------- 1 | name: Converted Workflow 2 | 'on': 3 | push: 4 | branches: 5 | - main 6 | pull_request: 7 | branches: 8 | - main 9 | jobs: 10 | test: 11 | runs-on: ubuntu-latest 12 | strategy: 13 | max-parallel: 1 14 | matrix: 15 | node-version: [18.x,20.x,22.x] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Use Node.js ${{ matrix.node-version }} 19 | uses: actions/setup-node@v4 20 | with: 21 | node-version: ${{ matrix.node-version }} 22 | cache: 'npm' 23 | - run: ls -lha ${{ github.workspace }} 24 | - run: npm run run-container 25 | - run: npm ci 26 | - run: sleep 5 27 | - run: npm run ci-coverage 28 | - name: Coveralls 29 | uses: coverallsapp/github-action@master 30 | with: 31 | github-token: ${{ secrets.GITHUB_TOKEN }} 32 | 33 | -------------------------------------------------------------------------------- /spec/behavior/emitter.js: -------------------------------------------------------------------------------- 1 | module.exports = (name) => { 2 | let handlers = {}; 3 | 4 | function raise (ev) { 5 | if (handlers[ev]) { 6 | const args = Array.prototype.slice.call(arguments, 1); 7 | handlers[ev].forEach(function (handler) { 8 | if (handler) { 9 | handler.apply(undefined, args); 10 | } 11 | }); 12 | } 13 | } 14 | 15 | function on (ev, handle) { 16 | if (handlers[ev]) { 17 | handlers[ev].push(handle); 18 | } else { 19 | handlers[ev] = [handle]; 20 | } 21 | return { 22 | unsubscribe: function (h) { 23 | handlers[ev].splice(handlers[ev].indexOf(h || handle)); // jshint ignore:line 24 | } 25 | }; 26 | } 27 | 28 | function reset () { 29 | handlers = {}; 30 | } 31 | 32 | return { 33 | name: name || 'default', 34 | handlers: handlers, 35 | on: on, 36 | once: on, 37 | raise: raise, 38 | reset: reset 39 | }; 40 | }; 41 | -------------------------------------------------------------------------------- /docs/logging.md: -------------------------------------------------------------------------------- 1 | ## Logging 2 | 3 | As of v2, logging uses [bole](https://github.com/rvagg/bole) because it defaults to machine parsable logs, 4 | which are minimalistic and easy to write stream adapters for. 5 | 6 | A DEBUG adapter that works just like before is already included in foo-foo-mq, 7 | so you can still prefix the service with `DEBUG=rabbot.*` to get foo-foo-mq specific output. 8 | 9 | > Note: `rabbot.queue.*` and `rabbot.exchange.*` are high volume namespaces 10 | > since that is where all published and subscribed messages get reported. 11 | 12 | ### Attaching Custom Loggers 13 | 14 | A log call is now exposed directly to make it easier to attach streams to the bole instance: 15 | 16 | ```javascript 17 | const rabbit = require( "foo-foo-mq" ); 18 | 19 | // works like bole's output call 20 | rabbit.log( [ 21 | { level: "info", stream: process.stdout }, 22 | { level: "debug", stream: fs.createWriteStream( "./debug.log" ), objectMode: true } 23 | ] ); 24 | ``` 25 | -------------------------------------------------------------------------------- /demo/pubsub/README.md: -------------------------------------------------------------------------------- 1 | This demo was built to show the following behaviors: 2 | 3 | * When to setup message handlers 4 | * Request/response 5 | * Publish/subscribe 6 | * Getting around timing issues with timers and message expiration 7 | * Using `configure` to provide all topology configuration at once 8 | * Sharing a common topology module amongst services 9 | 10 | ## 2.0.0 timings 11 | 12 | There are notable performance differences in 2.0.0: 13 | 14 | * recipient times don't "decay" as message counts increase 15 | * there aren't massive pauses when receving continuous message sets 16 | * this remains true even with publish confirmation on 17 | 18 | * 100 ~ 30 ms 19 | * 1000 ~ 215 ms 20 | * 10000 ~ 1600 ms 21 | * 15000 ~ 2.6 s 22 | * 100000 ~ 16 seconds 23 | 24 | ## 1.1.0 timings 25 | 26 | * 100 ~ 30 ms 27 | * 1000 ~ 150 ms 28 | * 10000 ~ 800 ms (sometimes low as 690, sometimes high as 820) 29 | * 15000 ~ 1.2 s 30 | * 100000 ~ 109 s 31 | 32 | every 30k or so HUGE PAUSE :@ costing roughly 10-20s 33 | -------------------------------------------------------------------------------- /RESOURCES.md: -------------------------------------------------------------------------------- 1 | ## Additional Learning Resources 2 | 3 | ### Watch Me Code 4 | Thanks to Derick Bailey's input, the API and documentation for rabbot have improved a lot. 5 | You can learn from Derick's hands-on experience in his [Watch Me Code] series. 6 | 7 | ### RabbitMQ In Action 8 | Alvaro Vidella and Jason Williams literally wrote the book on [RabbitMQ]. 9 | 10 | ### Enterprise Integration Patterns 11 | Gregor Hophe and Bobby Woolf's definitive work on messaging. 12 | The [site][EIP Site] provides basic descriptions of the patterns 13 | and the [book][EIP Book] goes into a lot of detail. 14 | 15 | I can't recommend this book highly enough; understanding the patterns will provide you with the conceptual tools need to be successful. 16 | 17 | 18 | [Watch Me Code]: https://sub.watchmecode.net/categories/rabbitm 19 | [RabbitMQ]: http://www.manning.com/videla/ 20 | [EIP Site]: http://www.enterpriseintegrationpatterns.com/ 21 | [EIP Book]: http://www.amazon.com/Enterprise-Integration-Patterns-Designing-Deploying/dp/0321200683 22 | -------------------------------------------------------------------------------- /demo/topic/publisher-topic.js: -------------------------------------------------------------------------------- 1 | // require( 'when/monitor/console' ); 2 | const rabbit = require('../../src/index.js'); 3 | 4 | // it can make a lot of sense to share topology definition across 5 | // services that will be using the same topology to avoid 6 | // scenarios where you have race conditions around when 7 | // exchanges, queues or bindings are in place 8 | require('./topology.js')(rabbit, null, 'default') 9 | .then(function () { 10 | console.log('EVERYTHING IS PEACHY'); 11 | publish(10000); 12 | }); 13 | 14 | rabbit.on('unreachable', function () { 15 | console.log(':('); 16 | process.exit(); 17 | }); 18 | 19 | function publish (total) { 20 | let i; 21 | 22 | const send = function (x) { 23 | const direction = (x % 2 === 0) ? 'left' : 'right'; 24 | rabbit.publish('topic-example-x', { 25 | routingKey: direction, 26 | type: direction, 27 | body: { 28 | message: 'Message ' + x 29 | } 30 | }).then(function () { 31 | console.log('published message', x); 32 | }); 33 | }; 34 | 35 | for (i = 0; i < total; i++) { 36 | send(i); 37 | } 38 | rabbit.shutdown(); 39 | } 40 | -------------------------------------------------------------------------------- /demo/topic/subscriber-topic-left.js: -------------------------------------------------------------------------------- 1 | module.exports = function (rabbit) { 2 | // variable to hold starting time 3 | const started = Date.now(); 4 | 5 | // variable to hold received count 6 | let received = 0; 7 | 8 | // expected message count 9 | const expected = 10000; 10 | 11 | // always setup your message handlers first 12 | 13 | // this handler will handle messages sent from the publisher 14 | rabbit.handle({ 15 | queue: 'topic-example-left-q', 16 | type: '#' 17 | }, function (msg) { 18 | console.log('LEFT Received:', JSON.stringify(msg.body), 'routingKey:', msg.fields.routingKey); 19 | msg.ack(); 20 | if ((++received) === expected) { 21 | console.log('LEFT Received', received, 'messages after', (Date.now() - started), 'milliseconds'); 22 | } 23 | }); 24 | 25 | // it can make a lot of sense to share topology definition across 26 | // services that will be using the same topology to avoid 27 | // scenarios where you have race conditions around when 28 | // exchanges, queues or bindings are in place 29 | require('./topology.js')(rabbit, 'left', 'left'); 30 | 31 | console.log('Set up LEFT OK'); 32 | }; 33 | -------------------------------------------------------------------------------- /demo/topic/subscriber-topic-right.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = function (rabbit) { 3 | // variable to hold starting time 4 | const started = Date.now(); 5 | 6 | // variable to hold received count 7 | let received = 0; 8 | 9 | // expected message count 10 | const expected = 10000; 11 | 12 | // always setup your message handlers first 13 | 14 | // this handler will handle messages sent from the publisher 15 | rabbit.handle({ 16 | queue: 'topic-example-right-q', 17 | type: '#' 18 | }, function (msg) { 19 | console.log('RIGHT Received:', JSON.stringify(msg.body), 'routingKey:', msg.fields.routingKey); 20 | msg.ack(); 21 | if ((++received) === expected) { 22 | console.log('RIGHT Received', received, 'messages after', (Date.now() - started), 'milliseconds'); 23 | } 24 | }); 25 | 26 | // it can make a lot of sense to share topology definition across 27 | // services that will be using the same topology to avoid 28 | // scenarios where you have race conditions around when 29 | // exchanges, queues or bindings are in place 30 | require('./topology.js')(rabbit, 'right', 'right'); 31 | 32 | console.log('Set up RIGHT OK'); 33 | }; 34 | -------------------------------------------------------------------------------- /src/amqp/channel.js: -------------------------------------------------------------------------------- 1 | const AmqpChannel = require('amqplib/lib/callback_model').Channel; 2 | const monad = require('./iomonad.js'); 3 | const log = require('../log')('rabbot.channel'); 4 | 5 | /* log 6 | * `rabbot.channel` 7 | * `debug` 8 | * when amqplib's `channel.close` promise is rejected 9 | */ 10 | 11 | function close (name, channel) { 12 | if (channel.close) { 13 | return channel.close() 14 | .then(null, function (err) { 15 | // since calling close on channel could reject the promise 16 | // (see connection close's comment) this catches and logs it 17 | // for debug level 18 | log.debug('Error was reported during close of connection `%s` - `%s`', name, err); 19 | }); 20 | } else { 21 | return Promise.resolve(); 22 | } 23 | } 24 | 25 | module.exports = { 26 | create: function (connection, name, confirm) { 27 | const method = confirm ? 'createConfirmChannel' : 'createChannel'; 28 | const factory = function () { 29 | return connection[method](); 30 | }; 31 | const channel = monad({ name: name }, 'channel', factory, AmqpChannel, close.bind(null, name)); 32 | return channel; 33 | } 34 | }; 35 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2016 Alex Robson 4 | Copyright (c) 2019 Zach Lintz 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in 14 | all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 22 | THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /demo/topic/topology.js: -------------------------------------------------------------------------------- 1 | module.exports = function (rabbit) { 2 | return rabbit.configure({ 3 | 4 | // arguments used to establish a connection to a broker 5 | connection: { 6 | user: 'guest', 7 | pass: 'guest', 8 | server: ['127.0.0.1'], 9 | port: 5672, 10 | vhost: '%2f', 11 | timeout: 1000, 12 | failAfter: 30, 13 | retryLimit: 400 14 | }, 15 | 16 | // define the exchanges 17 | exchanges: [{ 18 | name: 'topic-example-x', 19 | type: 'topic', 20 | autoDelete: true 21 | }], 22 | 23 | // setup the queues, only subscribing to the one this service 24 | // will consume messages from 25 | queues: [{ 26 | name: 'topic-example-left-q', 27 | autoDelete: true, 28 | subscribe: true 29 | }, { 30 | name: 'topic-example-right-q', 31 | autoDelete: true, 32 | subscribe: true 33 | }], 34 | 35 | // binds exchanges and queues to one another 36 | bindings: [{ 37 | exchange: 'topic-example-x', 38 | target: 'topic-example-left-q', 39 | keys: ['left'] 40 | }, { 41 | exchange: 'topic-example-x', 42 | target: 'topic-example-right-q', 43 | keys: ['right'] 44 | }] 45 | }).then(null, function (err) { 46 | console.error('Could not connect or configure:', err); 47 | }); 48 | }; 49 | -------------------------------------------------------------------------------- /spec/integration/connection.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Connection', function () { 6 | describe('on connection', function () { 7 | let connected; 8 | before(function (done) { 9 | rabbit.once('connected', (c) => { 10 | connected = c; 11 | done(); 12 | }); 13 | rabbit.configure({ connection: config.connection }); 14 | }); 15 | 16 | it('should assign uri to connection', function () { 17 | connected.uri.should.equal('amqp://guest:guest@127.0.0.1:5672/%2f?heartbeat=30&frameMax=4096'); 18 | }); 19 | 20 | after(function () { 21 | return rabbit.close('default'); 22 | }); 23 | }); 24 | 25 | describe('on connection using uri', function () { 26 | let connected; 27 | before(function (done) { 28 | rabbit.once('connected', (c) => { 29 | connected = c; 30 | done(); 31 | }); 32 | rabbit.addConnection({ name: 'connectionWithUri', uri: 'amqp://guest:guest@127.0.0.1:5672/%2f?heartbeat=11&frameMax=8192' }); 33 | }); 34 | 35 | it('should assign uri to connection', function () { 36 | connected.uri.should.equal('amqp://guest:guest@127.0.0.1:5672/%2f?heartbeat=11&frameMax=8192'); 37 | }); 38 | 39 | after(function () { 40 | return rabbit.close('connectionWithUri'); 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /spec/integration/noack.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Message Acknowledgments Disabled (noAck: true)', function () { 6 | let messagesToSend; 7 | let harness; 8 | 9 | before(function (done) { 10 | rabbit.configure({ 11 | connection: config.connection, 12 | exchanges: [ 13 | { 14 | name: 'rabbot-ex.no-ack', 15 | type: 'direct', 16 | autoDelete: true 17 | } 18 | ], 19 | queues: [ 20 | { 21 | name: 'rabbot-q.no-ack', 22 | autoDelete: true, 23 | subscribe: true, 24 | noAck: true, 25 | limit: 5 26 | } 27 | ], 28 | bindings: [ 29 | { 30 | exchange: 'rabbot-ex.no-ack', 31 | target: 'rabbot-q.no-ack' 32 | } 33 | ] 34 | }).then(() => { 35 | messagesToSend = 10; 36 | harness = harnessFactory(rabbit, done, messagesToSend); 37 | harness.handle('no.ack'); 38 | 39 | for (let i = 0; i < messagesToSend; i++) { 40 | rabbit.publish('rabbot-ex.no-ack', { 41 | type: 'no.ack', 42 | body: 'message ' + i, 43 | routingKey: '' 44 | }); 45 | } 46 | }); 47 | }); 48 | 49 | it('should receive all messages', function () { 50 | harness.received.length.should.equal(messagesToSend); 51 | }); 52 | 53 | after(function () { 54 | return harness.clean('default'); 55 | }); 56 | }); 57 | -------------------------------------------------------------------------------- /src/info.js: -------------------------------------------------------------------------------- 1 | const crypto = require('crypto'); 2 | const os = require('os'); 3 | const format = require('util').format; 4 | const self = require('../package.json'); 5 | 6 | const host = os.hostname(); 7 | const platform = os.platform(); 8 | const architecture = os.arch(); 9 | const title = process.title; 10 | const pid = process.pid; 11 | const consumerId = format('%s.%s.%s', host, title, pid); 12 | const consistentId = format('%s.%s', host, title); 13 | const toBE = os.endianness() === 'BE'; 14 | 15 | function createConsumerTag (queueName) { 16 | if (queueName.indexOf(consumerId) === 0) { 17 | return queueName; 18 | } else { 19 | return format('%s.%s', consumerId, queueName); 20 | } 21 | } 22 | 23 | function hash (id) { 24 | const bytes = crypto.createHash('md5').update(id).digest(); 25 | const num = toBE ? bytes.readdInt16BE() : bytes.readInt16LE(); 26 | return num < 0 ? Math.abs(num) + 0xffffffff : num; 27 | } 28 | 29 | // not great, but good enough for our purposes 30 | function createConsumerHash () { 31 | return hash(consumerId); 32 | } 33 | 34 | function createConsistentHash () { 35 | return hash(consistentId); 36 | } 37 | 38 | function getHostInfo () { 39 | return format('%s (%s %s)', host, platform, architecture); 40 | } 41 | 42 | function getProcessInfo () { 43 | return format('%s (pid: %d)', title, pid); 44 | } 45 | 46 | function getLibInfo () { 47 | return format('foo-foo-mq - %s', self.version); 48 | } 49 | 50 | module.exports = { 51 | id: consumerId, 52 | host: getHostInfo, 53 | lib: getLibInfo, 54 | process: getProcessInfo, 55 | createTag: createConsumerTag, 56 | createHash: createConsumerHash, 57 | createConsistentHash: createConsistentHash 58 | }; 59 | -------------------------------------------------------------------------------- /demo/pubsub/topology.js: -------------------------------------------------------------------------------- 1 | module.exports = function (rabbit, subscribeTo) { 2 | return rabbit.configure({ 3 | // arguments used to establish a connection to a broker 4 | connection: { 5 | user: 'guest', 6 | pass: 'guest', 7 | server: ['127.0.0.1'], 8 | port: 5672, 9 | vhost: '%2f', 10 | publishTimeout: 100, 11 | timeout: 1000, 12 | failAfter: 30, 13 | retryLimit: 400 14 | }, 15 | 16 | // define the exchanges 17 | exchanges: [ 18 | { 19 | name: 'wascally-pubsub-requests-x', 20 | type: 'direct', 21 | autoDelete: true 22 | }, 23 | { 24 | name: 'wascally-pubsub-messages-x', 25 | type: 'fanout', 26 | autoDelete: true 27 | } 28 | ], 29 | 30 | // setup the queues, only subscribing to the one this service 31 | // will consume messages from 32 | queues: [ 33 | { 34 | name: 'wascally-pubsub-requests-q', 35 | // autoDelete: true, 36 | durable: true, 37 | unique: 'hash', 38 | subscribe: subscribeTo === 'requests' 39 | }, 40 | { 41 | name: 'wascally-pubsub-messages-q', 42 | autoDelete: true, 43 | subscribe: subscribeTo === 'messages' 44 | } 45 | ], 46 | 47 | // binds exchanges and queues to one another 48 | bindings: [ 49 | { 50 | exchange: 'wascally-pubsub-requests-x', 51 | target: 'wascally-pubsub-requests-q', 52 | keys: [''] 53 | }, 54 | { 55 | exchange: 'wascally-pubsub-messages-x', 56 | target: 'wascally-pubsub-messages-q', 57 | keys: [] 58 | } 59 | ] 60 | }).then(null, function () {}); 61 | }; 62 | -------------------------------------------------------------------------------- /spec/integration/typeless.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | Demonstrates handling Messages With No Type Provided 7 | */ 8 | describe('No Type Handling', function () { 9 | let harness; 10 | 11 | before(function (done) { 12 | rabbit.configure({ 13 | connection: config.connection, 14 | exchanges: [ 15 | { 16 | name: 'rabbot-ex.topic', 17 | type: 'topic', 18 | alternate: 'rabbot-ex.alternate', 19 | autoDelete: true 20 | } 21 | ], 22 | queues: [ 23 | { 24 | name: 'rabbot-q.topic', 25 | autoDelete: true, 26 | subscribe: true, 27 | deadletter: 'rabbot-ex.deadletter' 28 | } 29 | ], 30 | bindings: [ 31 | { 32 | exchange: 'rabbot-ex.topic', 33 | target: 'rabbot-q.topic', 34 | keys: 'this.is.*' 35 | } 36 | ] 37 | }).then(() => { 38 | harness = harnessFactory(rabbit, done, 1); 39 | harness.handle('#.typeless'); 40 | rabbit.publish('rabbot-ex.topic', { type: '', routingKey: 'this.is.typeless', body: 'one' }); 41 | }); 42 | }); 43 | 44 | it('should handle messages based on the message topic instead of type', function () { 45 | const results = harness.received.map((m) => 46 | ({ 47 | body: m.body, 48 | key: m.fields.routingKey 49 | }) 50 | ); 51 | sortBy(results, 'body').should.eql( 52 | [ 53 | { body: 'one', key: 'this.is.typeless' } 54 | ]); 55 | }); 56 | 57 | after(function () { 58 | return harness.clean('default'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /spec/integration/nobatch.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Batch Acknowledgments Disabled (noBatch: true)', function () { 6 | let messagesToSend; 7 | let harness; 8 | 9 | before(function (done) { 10 | rabbit.configure({ 11 | connection: config.connection, 12 | exchanges: [ 13 | { 14 | name: 'rabbot-ex.no-batch', 15 | type: 'direct', 16 | autoDelete: true 17 | } 18 | ], 19 | queues: [ 20 | { 21 | name: 'rabbot-q.no-batch', 22 | autoDelete: true, 23 | subscribe: true, 24 | noBatch: true, 25 | limit: 5 26 | } 27 | ], 28 | bindings: [ 29 | { 30 | exchange: 'rabbot-ex.no-batch', 31 | target: 'rabbot-q.no-batch' 32 | } 33 | ] 34 | }).then(() => { 35 | messagesToSend = 10; 36 | harness = harnessFactory(rabbit, done, messagesToSend); 37 | let messageCount = 0; 38 | 39 | harness.handle('no.batch', (message) => { 40 | if (messageCount > 0) { 41 | message.ack(); 42 | } 43 | messageCount += 1; 44 | }); 45 | 46 | for (let i = 0; i < messagesToSend; i++) { 47 | rabbit.publish('rabbot-ex.no-batch', { 48 | type: 'no.batch', 49 | body: 'message ' + i, 50 | routingKey: '' 51 | }); 52 | } 53 | }); 54 | }); 55 | 56 | it('should receive all messages', function () { 57 | harness.received.length.should.equal(messagesToSend); 58 | }); 59 | 60 | after(function () { 61 | return harness.clean('default'); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /ACKNOWLEDGEMENTS.md: -------------------------------------------------------------------------------- 1 | ## Special Thanks To 2 | 3 | Several folks have contributed time, effort, ideas or small PRs to the legacy of this project. 4 | It would not be what it is without them! 5 | 6 | * [Ryan Niemeyer](http://knockmeout.net) 7 | * [Nathan Graves](https://github.com/woolite64) 8 | * [Ben Whatley](https://github.com/darklordzw) 9 | * [Randy Groff](http://randygroff.com) 10 | * [Joseph Frazier](https://github.com/josephfrazier) 11 | * [Sean Corrales](https://github.com/droidenator) 12 | * [Matthew Martin](http://matmar10.com) 13 | * [Tom Kirkpatrick](https://github.com/mrfelton) 14 | * [Bill Matson](https://github.com/bmatson) 15 | * [Scott Walters](http://github.com/LeankitScott) 16 | * [Eric Satterwhite](http://codedependant.net/) 17 | * [Leonardo Bispo de Oliveira](http://blog.bispooliveira.de) 18 | * [Michael Tuttle](https://github.com/openam) 19 | * [Dj Walker-Morgan](http://www.codepope.com) 20 | * [Hugo Cortes](https://github.com/hugocortes) 21 | * [Mathias Lundell](https://github.com/luddd3) 22 | 23 | ### Rabbot 24 | 25 | We cannot thank Alex Robson enough for the time and effort he put into Rabbot, the predecessor of foo-foo-mq. 26 | Without him, this library would not be what it is today. 27 | We hope that with time we can build on his legacy as successfully as he was able to build on the successes of his predecessors. 28 | 29 | ### In Memoriam 30 | 31 | Austin Young was an exceptionally bright software engineer that made material contributions to Wascally (the predecessor) which remain a big part of Rabbot today. Austin took on the challenging and somewhat tedious task of building the first pass at batch acknowledgements and the approach to testing them. 32 | 33 | I remain grateful to Austin for his enthusiastic contribution. 34 | -------------------------------------------------------------------------------- /spec/integration/publish.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | 4 | describe('Publishing Messages', function () { 5 | describe('without a connection defined', function () { 6 | it('should reject publish call with missing connection', function () { 7 | return rabbit.publish('', { type: 'nothing', routingKey: '', body: '', connectionName: 'notthere' }) 8 | .should.be.rejectedWith('Publish failed - no connection notthere has been configured'); 9 | }); 10 | }); 11 | 12 | describe('with a connection and no exchange defined', function () { 13 | it('should reject publish call with missing exchange', function () { 14 | rabbit.addConnection({}); 15 | return rabbit.publish('missing.ex', { type: 'nothing', routingKey: '', body: '' }) 16 | .should.be.rejectedWith('Publish failed - no exchange missing.ex on connection default is defined'); 17 | }); 18 | 19 | after(function () { 20 | rabbit.reset(); 21 | return rabbit.shutdown(); 22 | }); 23 | }); 24 | 25 | describe('with a connection and exchange defined', function () { 26 | it('should not error on publish calls', function () { 27 | rabbit.configure({ 28 | connection: { 29 | name: 'temp' 30 | }, 31 | exchanges: { 32 | name: 'simple.ex', 33 | type: 'direct', 34 | autoDelete: true 35 | } 36 | }); 37 | return rabbit.publish('simple.ex', { type: 'nothing', routingKey: '', body: '', connectionName: 'temp' }); 38 | }); 39 | 40 | after(function () { 41 | return rabbit.deleteExchange('simple.ex', 'temp') 42 | .then(() => { 43 | rabbit.reset(); 44 | return rabbit.shutdown(); 45 | }); 46 | }); 47 | }); 48 | }); 49 | -------------------------------------------------------------------------------- /spec/integration/noReplyQueue.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | function stallLongEnoughToARegisterMessages () { 6 | return new Promise((resolve, reject) => { 7 | setTimeout(() => { 8 | resolve(); 9 | }, 600); 10 | }); 11 | } 12 | 13 | describe('No Reply Queue (replyQueue: false)', function () { 14 | let messagesToSend; 15 | let harness; 16 | 17 | before(function (done) { 18 | harness = harnessFactory(rabbit, done, messagesToSend); 19 | rabbit.configure({ 20 | connection: config.noReplyQueue, 21 | exchanges: [ 22 | { 23 | name: 'noreply-ex.direct', 24 | type: 'direct', 25 | autoDelete: true 26 | } 27 | ], 28 | queues: [ 29 | { 30 | name: 'noreply-q.direct', 31 | autoDelete: true, 32 | subscribe: true 33 | } 34 | ], 35 | bindings: [ 36 | { 37 | exchange: 'noreply-ex.direct', 38 | target: 'noreply-q.direct', 39 | keys: '' 40 | } 41 | ] 42 | }).then(() => { 43 | messagesToSend = 3; 44 | harness.handle('no.replyQueue'); 45 | for (let i = 0; i < messagesToSend; i++) { 46 | rabbit.publish('noreply-ex.direct', { 47 | connectionName: 'noReplyQueue', 48 | type: 'no.replyQueue', 49 | body: 'message ' + i, 50 | routingKey: '' 51 | }); 52 | } 53 | return stallLongEnoughToARegisterMessages(); 54 | }); 55 | }); 56 | 57 | it('should receive all messages', function () { 58 | harness.received.length.should.equal(messagesToSend); 59 | }); 60 | 61 | after(function () { 62 | return harness.clean('noReplyQueue'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /spec/integration/fanout.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Fanout Exchange With Multiple Subscribed Queues', function () { 6 | let harness; 7 | 8 | before(function (done) { 9 | rabbit.configure({ 10 | connection: config.connection, 11 | exchanges: [ 12 | { 13 | name: 'rabbot-ex.fanout', 14 | type: 'fanout', 15 | autoDelete: true 16 | } 17 | ], 18 | queues: [ 19 | { 20 | name: 'rabbot-q.general1', 21 | autoDelete: true, 22 | subscribe: true 23 | }, 24 | { 25 | name: 'rabbot-q.general2', 26 | noAck: true, 27 | autoDelete: true, 28 | subscribe: true 29 | } 30 | ], 31 | bindings: [ 32 | { 33 | exchange: 'rabbot-ex.fanout', 34 | target: 'rabbot-q.general1', 35 | keys: [] 36 | }, 37 | { 38 | exchange: 'rabbot-ex.fanout', 39 | target: 'rabbot-q.general2', 40 | keys: [] 41 | } 42 | ] 43 | }).then(() => { 44 | rabbit.publish('rabbot-ex.fanout', { type: 'fanned', routingKey: 'this.is.ignored', body: 'hello, everyone' }); 45 | }); 46 | 47 | harness = harnessFactory(rabbit, done, 2); 48 | harness.handle('fanned'); 49 | }); 50 | 51 | it('should handle messages on all subscribed queues', function () { 52 | const results = harness.received.map((m) => ({ 53 | body: m.body, 54 | key: m.fields.routingKey 55 | })); 56 | sortBy(results, 'body').should.eql( 57 | [ 58 | { body: 'hello, everyone', key: 'this.is.ignored' }, 59 | { body: 'hello, everyone', key: 'this.is.ignored' } 60 | ]); 61 | }); 62 | 63 | after(function () { 64 | return harness.clean('default'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /spec/integration/randomQueue.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | Demonstrates a few things: 7 | * Getting a random queue name from Rabbit 8 | * Publishing to the default exchange 9 | * Using the queue name as a routing key to 10 | deliver the message to the queue 11 | 12 | It shows that you _can_ move messages between services with minimal configuration. 13 | */ 14 | describe('Random Queue Name', function () { 15 | let harness; 16 | let queueName; 17 | before((done) => { 18 | rabbit.configure({ 19 | connection: config.connection, 20 | exchanges: [ 21 | ], 22 | queues: [ 23 | ], 24 | bindings: [ 25 | ] 26 | }).then(() => { 27 | harness.handle('rando', undefined, queueName); 28 | rabbit.addQueue('', { autoDelete: true, subscribe: true }) 29 | .then(function (queue) { 30 | queueName = queue.name; 31 | rabbit.publish('', { type: 'rando', routingKey: queueName, body: 'one' }); 32 | rabbit.publish('', { type: 'rando', routingKey: queueName, body: Buffer.from('two') }); 33 | rabbit.publish('', { type: 'rando', routingKey: queueName, body: [0x62, 0x75, 0x66, 0x66, 0x65, 0x72] }); 34 | }); 35 | }); 36 | harness = harnessFactory(rabbit, done, 3); 37 | }); 38 | 39 | it('should deliver all messages to the randomly generated queue', function () { 40 | const results = harness.received.map((m) => ({ 41 | body: m.body.toString(), 42 | queue: m.queue 43 | })); 44 | sortBy(results, 'body').should.eql( 45 | [ 46 | { body: '98,117,102,102,101,114', queue: queueName }, 47 | { body: 'one', queue: queueName }, 48 | { body: 'two', queue: queueName } 49 | ]); 50 | }); 51 | 52 | after(function () { 53 | return harness.clean('default'); 54 | }); 55 | }); 56 | -------------------------------------------------------------------------------- /spec/integration/addPassiveQueue.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | 4 | describe('Adding Queues', function () { 5 | describe('when the queue does not already exist', function () { 6 | it('should error on addQueue in passive mode', function () { 7 | return rabbit.configure({ 8 | connection: { 9 | name: 'passiveErrorWithNoQueue' 10 | } 11 | }).then(() => { 12 | return rabbit.addQueue('no-queue-here', { passive: true }, 'passiveErrorWithNoQueue') 13 | .then( 14 | () => { throw new Error('Should not have succeeded in the checkQueue call'); }, 15 | (err) => { 16 | err.toString().should.contain("Failed to create queue 'no-queue-here' on connection 'passiveErrorWithNoQueue' with 'Error: Operation failed: QueueDeclare; 404 (NOT-FOUND)"); 17 | }); 18 | }); 19 | }); 20 | after(function () { 21 | rabbit.reset(); 22 | return rabbit.shutdown('passiveErrorWithNoQueue'); 23 | }); 24 | }); 25 | 26 | describe('when the queue does exist', function () { 27 | const existingQueueName = 'totes-exists-already'; 28 | it('should NOT error on addQueue when in passive mode', function () { 29 | return rabbit.configure({ 30 | connection: { 31 | name: 'passiveEnabledWithExistingQueue' 32 | }, 33 | queues: [ 34 | { name: existingQueueName, connection: 'passiveEnabledWithExistingQueue' } 35 | ] 36 | }).then(() => { 37 | return rabbit.addQueue(existingQueueName, { passive: true }, 'passiveEnabledWithExistingQueue'); 38 | }); 39 | }); 40 | 41 | after(function () { 42 | return rabbit.deleteQueue(existingQueueName, 'passiveEnabledWithExistingQueue') 43 | .then(() => { 44 | rabbit.reset(); 45 | return rabbit.shutdown(); 46 | }); 47 | }); 48 | }); 49 | }); 50 | -------------------------------------------------------------------------------- /spec/integration/mandatory.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | This specificationd demonstrates the returned callback strategy. 7 | The harness provides a default returned handler that captures 8 | returned messages and adds them to a list. 9 | */ 10 | describe('Undeliverable & Mandatory: true', function () { 11 | let harness; 12 | 13 | before(function (done) { 14 | rabbit.configure({ 15 | connection: config.connection, 16 | exchanges: [ 17 | { 18 | name: 'rabbot-ex.direct', 19 | type: 'direct', 20 | autoDelete: true 21 | } 22 | ], 23 | queues: [ 24 | { 25 | name: 'rabbot-q.direct', 26 | autoDelete: true, 27 | subscribe: true 28 | } 29 | ], 30 | bindings: [ 31 | { 32 | exchange: 'rabbot-ex.direct', 33 | target: 'rabbot-q.direct', 34 | keys: [] 35 | } 36 | ] 37 | }).then(() => { 38 | rabbit.publish('rabbot-ex.direct', { mandatory: true, routingKey: 'completely.un.routable.1', body: 'returned message #1' }); 39 | rabbit.publish('rabbot-ex.direct', { mandatory: true, routingKey: 'completely.un.routable.2', body: 'returned message #2' }); 40 | }); 41 | 42 | harness = harnessFactory(rabbit, done, 2); 43 | }); 44 | 45 | it('should capture all unhandled messages via custom unhandled message strategy', function () { 46 | const results = harness.returned.map((m) => ({ 47 | type: m.type, 48 | body: m.body 49 | })); 50 | sortBy(results, 'body').should.eql( 51 | [ 52 | { body: 'returned message #1', type: 'completely.un.routable.1' }, 53 | { body: 'returned message #2', type: 'completely.un.routable.2' } 54 | ]); 55 | }); 56 | 57 | after(function () { 58 | return harness.clean('default'); 59 | }); 60 | }); 61 | -------------------------------------------------------------------------------- /spec/integration/directReplyQueue.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Direct Reply Queue (replyQueue: \'rabbit\')', function () { 6 | let messagesToSend; 7 | let harness; 8 | const replies = []; 9 | 10 | before(function (done) { 11 | harness = harnessFactory(rabbit, () => {}, messagesToSend); 12 | rabbit.configure({ 13 | connection: config.directReplyQueue, 14 | exchanges: [ 15 | { 16 | name: 'noreply-ex.direct', 17 | type: 'direct', 18 | autoDelete: true 19 | } 20 | ], 21 | queues: [ 22 | { 23 | name: 'noreply-q.direct', 24 | autoDelete: true, 25 | subscribe: true 26 | } 27 | ], 28 | bindings: [ 29 | { 30 | exchange: 'noreply-ex.direct', 31 | target: 'noreply-q.direct', 32 | keys: '' 33 | } 34 | ] 35 | }).then(() => { 36 | messagesToSend = 3; 37 | harness.handle('no.replyQueue', (req) => { 38 | req.reply({ reply: req.body.message }); 39 | }); 40 | for (let i = 0; i < messagesToSend; i++) { 41 | rabbit.request('noreply-ex.direct', { 42 | connectionName: 'directReplyQueue', 43 | type: 'no.replyQueue', 44 | body: { message: i }, 45 | routingKey: '' 46 | }) 47 | .then( 48 | r => { 49 | replies.push(r.body.reply); 50 | r.ack(); 51 | if (replies.length >= messagesToSend) { 52 | done(); 53 | } 54 | } 55 | ); 56 | } 57 | }); 58 | }); 59 | 60 | it('should receive all replies', function () { 61 | harness.received.length.should.equal(messagesToSend); 62 | replies.should.eql([0, 1, 2]); 63 | }); 64 | 65 | after(function () { 66 | return harness.clean('directReplyQueue'); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /spec/integration/unrouted.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Unroutable Messages - Alternate Exchanges', function () { 6 | let harness; 7 | 8 | before(function (done) { 9 | rabbit.configure({ 10 | connection: config.connection, 11 | exchanges: [ 12 | { 13 | name: 'rabbot-ex.deadend', 14 | type: 'fanout', 15 | alternate: 'rabbot-ex.alternate', 16 | autoDelete: true 17 | }, 18 | { 19 | name: 'rabbot-ex.alternate', 20 | type: 'fanout', 21 | autoDelete: true 22 | } 23 | ], 24 | queues: [ 25 | { 26 | name: 'rabbot-q.alternate', 27 | autoDelete: true, 28 | subscribe: true 29 | } 30 | ], 31 | bindings: [ 32 | { 33 | exchange: 'rabbot-ex.alternate', 34 | target: 'rabbot-q.alternate', 35 | keys: [] 36 | } 37 | ] 38 | }).then(() => { 39 | rabbit.publish('rabbot-ex.deadend', { type: 'deadend', routingKey: 'empty', body: 'one' }); 40 | rabbit.publish('rabbot-ex.deadend', { type: 'deadend', routingKey: 'nothing', body: 'two' }); 41 | rabbit.publish('rabbot-ex.deadend', { type: 'deadend', routingKey: 'de.nada', body: 'three' }); 42 | }); 43 | 44 | harness = harnessFactory(rabbit, done, 3); 45 | harness.handle('deadend'); 46 | }); 47 | 48 | it('should capture all unrouted messages via the alternate exchange and queue', function () { 49 | const results = harness.received.map((m) => ({ 50 | body: m.body, 51 | key: m.fields.routingKey 52 | })); 53 | sortBy(results, 'body').should.eql( 54 | [ 55 | { body: 'one', key: 'empty' }, 56 | { body: 'three', key: 'de.nada' }, 57 | { body: 'two', key: 'nothing' } 58 | ]); 59 | }); 60 | 61 | after(function () { 62 | return harness.clean('default'); 63 | }); 64 | }); 65 | -------------------------------------------------------------------------------- /demo/pubsub/publisher.js: -------------------------------------------------------------------------------- 1 | const rabbit = require('../../src/index.js'); 2 | const fs = require('fs'); 3 | 4 | rabbit.log( 5 | { level: 'debug', stream: fs.createWriteStream('./debug.log'), objectMode: true } 6 | ); 7 | // always setup your message handlers first 8 | 9 | // this handler will respond to the subscriber request and trigger 10 | // sending a bunch of messages 11 | rabbit.handle('subscriber.request', function (msg) { 12 | console.log('Got subscriber request'); 13 | // replying to the message also ack's it to the queue 14 | msg.reply({ getReady: 'forawesome' }, 'publisher.response'); 15 | setTimeout(() => publish(msg.body.batchSize, msg.body.expected), 0); 16 | }); 17 | 18 | // it can make a lot of sense to share topology definition across 19 | // services that will be using the same topology to avoid 20 | // scenarios where you have race conditions around when 21 | // exchanges, queues or bindings are in place 22 | require('./topology.js')(rabbit, 'requests') 23 | .then(function (x) { 24 | console.log('ready'); 25 | }); 26 | 27 | rabbit.on('unreachable', function () { 28 | console.log(':('); 29 | process.exit(); 30 | }); 31 | 32 | function publish (batchSize, total) { 33 | let subtotal = total; 34 | if (total > batchSize) { 35 | subtotal = batchSize; 36 | } 37 | const pending = new Array(subtotal); 38 | total -= subtotal; 39 | let lost = 0; 40 | for (let i = 0; i < subtotal; i++) { 41 | pending.push( 42 | rabbit.publish('wascally-pubsub-messages-x', { 43 | type: 'publisher.message', 44 | body: { message: `Message ${i}` } 45 | }).then( 46 | null, 47 | (e) => { 48 | lost++; 49 | throw e; 50 | } 51 | ) 52 | ); 53 | } 54 | if (total > 0) { 55 | Promise.all(pending) 56 | .then(() => { 57 | console.log(`just published ${batchSize} messages ... boy are my arms tired?`); 58 | setTimeout(() => publish(batchSize, total), 0); 59 | }, 60 | () => { 61 | console.log(`${lost} MESSAGES LOST!`); 62 | setTimeout(() => publish(batchSize, total), 0); 63 | }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /spec/integration/subscription.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | A promise, twice made, is not a promise for more, 7 | it's simply reassurance for the insecure. 8 | */ 9 | describe('Duplicate Subscription', function () { 10 | let harness; 11 | 12 | before(function (done) { 13 | rabbit.configure({ 14 | connection: config.connection, 15 | exchanges: [ 16 | { 17 | name: 'rabbot-ex.subscription', 18 | type: 'topic', 19 | alternate: 'rabbot-ex.alternate', 20 | autoDelete: true 21 | } 22 | ], 23 | queues: [ 24 | { 25 | name: 'rabbot-q.subscription', 26 | autoDelete: true, 27 | subscribe: true, 28 | deadletter: 'rabbot-ex.deadletter' 29 | } 30 | ], 31 | bindings: [ 32 | { 33 | exchange: 'rabbot-ex.subscription', 34 | target: 'rabbot-q.subscription', 35 | keys: 'this.is.#' 36 | } 37 | ] 38 | }).then(() => { 39 | harness.handle('topic'); 40 | rabbit.startSubscription('rabbot-q.subscription'); 41 | rabbit.publish('rabbot-ex.subscription', { type: 'topic', routingKey: 'this.is.a.test', body: 'broadcast' }); 42 | rabbit.publish('rabbot-ex.subscription', { type: 'topic', routingKey: 'this.is.sparta', body: 'leonidas' }); 43 | rabbit.publish('rabbot-ex.subscription', { type: 'topic', routingKey: 'this.is.not.wine.wtf', body: 'socrates' }); 44 | }); 45 | harness = harnessFactory(rabbit, done, 3); 46 | }); 47 | 48 | it('should handle all messages once', function () { 49 | const results = harness.received.map((m) => 50 | ({ 51 | body: m.body, 52 | key: m.fields.routingKey 53 | }) 54 | ); 55 | sortBy(results, 'body').should.eql( 56 | [ 57 | { body: 'broadcast', key: 'this.is.a.test' }, 58 | { body: 'leonidas', key: 'this.is.sparta' }, 59 | { body: 'socrates', key: 'this.is.not.wine.wtf' } 60 | ]); 61 | }); 62 | 63 | after(function () { 64 | return harness.clean('default'); 65 | }); 66 | }); 67 | -------------------------------------------------------------------------------- /src/publishLog.js: -------------------------------------------------------------------------------- 1 | const defer = require('./defer'); 2 | 3 | function add (state, m) { 4 | if (!state.messages.sequenceNo) { 5 | const mSeq = next(state); 6 | m.sequenceNo = mSeq; 7 | state.messages[mSeq] = m; 8 | } 9 | } 10 | 11 | function next (state) { 12 | state.count++; 13 | return (state.sequenceNumber++); 14 | } 15 | 16 | function getEmptyPromise (state) { 17 | if (state.count) { 18 | const deferred = defer(); 19 | state.waiting = deferred; 20 | return deferred.promise; 21 | } else { 22 | return Promise.resolve(); 23 | } 24 | } 25 | 26 | function resolveWaiting (state) { 27 | if (state.waiting) { 28 | setTimeout(function () { 29 | state.waiting.resolve(state.count); 30 | state.waiting = undefined; 31 | }, state.sequenceNumber); 32 | } 33 | } 34 | 35 | function rejectWaiting (state) { 36 | if (state.waiting) { 37 | state.waiting.reject(); 38 | state.waiting = undefined; 39 | } 40 | } 41 | 42 | function remove (state, m) { 43 | const mSeq = m.sequenceNo !== undefined ? m.sequenceNo : m; 44 | let removed = false; 45 | if (state.messages[mSeq]) { 46 | delete state.messages[mSeq]; 47 | state.count--; 48 | removed = true; 49 | } 50 | if (state.count === 0) { 51 | resolveWaiting(state); 52 | } 53 | return removed; 54 | } 55 | 56 | function reset (state, err) { 57 | const keys = Object.keys(state.messages); 58 | const list = keys.map((key) => { 59 | const m = state.messages[key]; 60 | delete m.sequenceNo; 61 | return m; 62 | }); 63 | state.sequenceNumber = 0; 64 | state.messages = {}; 65 | state.count = 0; 66 | rejectWaiting(state); 67 | return list; 68 | } 69 | 70 | function publishLog () { 71 | const state = { 72 | count: 0, 73 | messages: {}, 74 | sequenceNumber: 0, 75 | waiting: undefined 76 | }; 77 | 78 | return { 79 | add: add.bind(undefined, state), 80 | count: function () { 81 | return Object.keys(state.messages).length; 82 | }, 83 | onceEmptied: getEmptyPromise.bind(undefined, state), 84 | reset: reset.bind(undefined, state), 85 | remove: remove.bind(undefined, state), 86 | state: state 87 | }; 88 | } 89 | 90 | module.exports = publishLog; 91 | -------------------------------------------------------------------------------- /spec/integration/typeSpecific.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | Demonstrates handling by type specification from *any* queue 7 | */ 8 | describe('Type Handling On Any Queue', function () { 9 | let harness; 10 | 11 | before(function (done) { 12 | rabbit.configure({ 13 | connection: config.connection, 14 | exchanges: [ 15 | { 16 | name: 'rabbot-ex.topic', 17 | type: 'topic', 18 | alternate: 'rabbot-ex.alternate', 19 | autoDelete: true 20 | } 21 | ], 22 | queues: [ 23 | { 24 | name: 'rabbot-q.topic-1', 25 | autoDelete: true, 26 | subscribe: true, 27 | deadletter: 'rabbot-ex.deadletter', 28 | type: 'classic' 29 | }, 30 | { 31 | name: 'rabbot-q.topic-2', 32 | autoDelete: true, 33 | subscribe: true, 34 | deadletter: 'rabbot-ex.deadletter', 35 | type: 'classic' 36 | } 37 | ], 38 | bindings: [ 39 | { 40 | exchange: 'rabbot-ex.topic', 41 | target: 'rabbot-q.topic-1', 42 | keys: 'Type.A' 43 | }, 44 | { 45 | exchange: 'rabbot-ex.topic', 46 | target: 'rabbot-q.topic-1', 47 | keys: 'Type.B' 48 | } 49 | ] 50 | }).then(() => { 51 | harness = harnessFactory(rabbit, done, 2); 52 | harness.handle('Type.*'); 53 | Promise.all([ 54 | rabbit.publish('rabbot-ex.topic', { type: 'Type.A', body: 'one' }), 55 | rabbit.publish('rabbot-ex.topic', { type: 'Type.B', body: 'two' }) 56 | ]); 57 | }); 58 | }); 59 | 60 | it('should handle messages based on the message type', function () { 61 | const results = harness.received.map((m) => 62 | ({ 63 | body: m.body, 64 | key: m.fields.routingKey 65 | }) 66 | ); 67 | sortBy(results, 'body').should.eql( 68 | [ 69 | { body: 'one', key: 'Type.A' }, 70 | { body: 'two', key: 'Type.B' } 71 | ]); 72 | }); 73 | 74 | after(function () { 75 | return harness.clean('default'); 76 | }); 77 | }); 78 | -------------------------------------------------------------------------------- /spec/setup.js: -------------------------------------------------------------------------------- 1 | const chai = require('chai'); 2 | chai.use(require('chai-as-promised')); 3 | global.should = chai.should(); 4 | global.expect = chai.expect; 5 | global.sinon = require('sinon'); 6 | process.title = 'rabbot-test'; 7 | 8 | global.harnessFactory = function (rabbit, cb, expected) { 9 | let handlers = []; 10 | let received = []; 11 | const unhandled = []; 12 | const returned = []; 13 | expected = expected || 1; 14 | const check = () => { 15 | if ((received.length + unhandled.length + returned.length) === expected) { 16 | cb(); 17 | } 18 | }; 19 | 20 | function defaultHandle (message) { 21 | message.ack(); 22 | } 23 | 24 | function wrap (handle) { 25 | return (message) => { 26 | handle(message); 27 | received.push(message); 28 | check(); 29 | }; 30 | } 31 | 32 | function handleFn (type, handle, queueName) { 33 | if (typeof type === 'object') { 34 | const options = type; 35 | options.handler = wrap(options.handler || defaultHandle); 36 | handlers.push(rabbit.handle(options)); 37 | } else { 38 | handlers.push(rabbit.handle(type, wrap(handle || defaultHandle), queueName)); 39 | } 40 | } 41 | 42 | function clean (connectionName) { 43 | handlers.forEach((handle) => { 44 | handle.remove(); 45 | }); 46 | handlers = []; 47 | received = []; 48 | if (connectionName) { 49 | return rabbit.close(connectionName, true); 50 | } 51 | } 52 | 53 | rabbit.onUnhandled((message) => { 54 | unhandled.push(message); 55 | message.ack(); 56 | check(); 57 | }); 58 | 59 | rabbit.onReturned((message) => { 60 | returned.push(message); 61 | check(); 62 | }); 63 | 64 | return { 65 | add: (msg) => { 66 | received.push(msg); 67 | check(); 68 | }, 69 | received: received, 70 | clean: clean, 71 | handle: handleFn, 72 | handlers: handlers, 73 | unhandled: unhandled, 74 | returned: returned 75 | }; 76 | }; 77 | 78 | global.sortBy = function (list, prop) { 79 | list.sort((a, b) => { 80 | if (a[prop] < b[prop]) { 81 | return -1; 82 | } else if (a[prop] > b[prop]) { 83 | return 1; 84 | } else { 85 | return 0; 86 | } 87 | }); 88 | return list; 89 | }; 90 | -------------------------------------------------------------------------------- /docs/notwascally.md: -------------------------------------------------------------------------------- 1 | ## Differences from `wascally` 2 | 3 | If you used wascally, foo-foo-mq's API will be familiar, but the behavior is quite different. 4 | This section explains the differences in behavior and design. 5 | 6 | ### Let it fail 7 | 8 | A great deal of confusion and edge cases arise from how wascally managed connectivity. 9 | Wascally treated any loss of connection or channels equally. 10 | This made it hard to predict behavior as a user of the library 11 | since any action taken against the API could trigger reconnection after an intentional shutdown. 12 | It also made it impossible to know whether a user intended to reconnect a closed connection 13 | or if the reconnection was the result of a programming error. 14 | 15 | foo-foo-mq does not re-establish connectivity automatically after connections have been intentionally closed _or_ after a failure threshold has been passed. 16 | In either of these cases, making API calls will either lead to rejected or indefinitely deferred promises. 17 | You, the user, must intentionally re-establish connectivity after closing a connection _or_ once foo-foo-mq has exhausted its attempts to connect on your behalf. 18 | 19 | *The recommendation is*: if foo-foo-mq tells you it can't reach rabbit after exhausting the configured retries, 20 | shut your service down and let your monitoring and alerting tell you about it. 21 | The code isn't going to fix a network or broker outage by retrying indefinitely and filling up your logs. 22 | 23 | ### No more indefinite retention of unpublished messages 24 | 25 | Wascally retained published messages indefinitely until a connection and all topology could be established. 26 | This meant that a service unable to connect could produce messages until it ran out of memory. 27 | It also meant that wascally could reject the promise returned from the publish call 28 | but then later publish the message without the ability to inform the caller. 29 | 30 | When a connection is lost, or the `unreachable` event is emitted, all promises for publish calls are rejected, 31 | and all unpublished messages are flushed. 32 | foo-foo-mq will not provide any additional features around unpublishable messages. 33 | There are no good one-size-fits-all behaviors in these failure scenarios, 34 | and it is important that developers understand and solve these needs at the service level for their use case. 35 | -------------------------------------------------------------------------------- /spec/integration/poisonMessages.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | When garbage is in the queue from a publisher 7 | rabbot should reject the unprocessable/busted 8 | message instead of melting down the process 9 | */ 10 | describe('Invalid Message Format', function () { 11 | let harness; 12 | 13 | before(function (done) { 14 | rabbit.configure({ 15 | connection: config.connection, 16 | exchanges: [ 17 | { 18 | name: 'rabbot-ex.fanout', 19 | type: 'fanout', 20 | autoDelete: true 21 | }, 22 | { 23 | name: 'poison-ex', 24 | type: 'fanout', 25 | autoDelete: true 26 | } 27 | ], 28 | queues: [ 29 | { 30 | name: 'rabbot-q.general1', 31 | autoDelete: true, 32 | subscribe: true, 33 | deadletter: 'poison-ex' 34 | }, 35 | { 36 | name: 'rabbot-q.poison', 37 | noAck: true, 38 | autoDelete: true, 39 | subscribe: true, 40 | poison: true 41 | } 42 | ], 43 | bindings: [ 44 | { 45 | exchange: 'rabbot-ex.fanout', 46 | target: 'rabbot-q.general1', 47 | keys: [] 48 | }, 49 | { 50 | exchange: 'poison-ex', 51 | target: 'rabbot-q.poison', 52 | keys: [] 53 | } 54 | ] 55 | }).then(() => { 56 | rabbit.publish('rabbot-ex.fanout', { 57 | type: 'yuck', 58 | routingKey: '', 59 | body: 'lol{":parse this', 60 | contentType: 'application/json' 61 | }); 62 | }); 63 | 64 | harness = harnessFactory(rabbit, done, 1); 65 | harness.handle('yuck.quarantined'); 66 | }); 67 | 68 | it('should have quarantined messages in unhandled', function () { 69 | const results = harness.received.map((m) => ({ 70 | body: m.body.toString(), 71 | key: m.fields.routingKey, 72 | quarantined: m.quarantined 73 | })); 74 | sortBy(results, 'body').should.eql( 75 | [ 76 | { 77 | key: '', 78 | body: 'lol{":parse this', 79 | quarantined: true 80 | } 81 | ] 82 | ); 83 | }); 84 | 85 | after(function () { 86 | return harness.clean('default'); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /src/config.js: -------------------------------------------------------------------------------- 1 | const log = require('./log')('rabbot.configuration'); 2 | 3 | /* log 4 | * `rabbot.configuration` 5 | * error 6 | * configuration failed (in exchange, queue or bindings) 7 | */ 8 | 9 | module.exports = function (Broker) { 10 | Broker.prototype.configure = function (config) { 11 | const emit = this.emit.bind(this); 12 | const configName = config.name || 'default'; 13 | this.configurations[configName] = config; 14 | this.configuring[configName] = new Promise(function (resolve, reject) { 15 | function onExchangeError (connection, err) { 16 | log.error('Configuration of %s failed due to an error in one or more exchange settings: %s', connection.name, err); 17 | reject(err); 18 | } 19 | 20 | function onQueueError (connection, err) { 21 | log.error('Configuration of %s failed due to an error in one or more queue settings: %s', connection.name, err.stack); 22 | reject(err); 23 | } 24 | 25 | function onBindingError (connection, err) { 26 | log.error('Configuration of %s failed due to an error in one or more bindings: %s', connection.name, err.stack); 27 | reject(err); 28 | } 29 | 30 | function createExchanges (connection) { 31 | connection.configureExchanges(config.exchanges) 32 | .then( 33 | createQueues.bind(null, connection), 34 | onExchangeError.bind(null, connection) 35 | ); 36 | } 37 | 38 | function createQueues (connection) { 39 | connection.configureQueues(config.queues) 40 | .then( 41 | createBindings.bind(null, connection), 42 | onQueueError.bind(null, connection) 43 | ); 44 | } 45 | 46 | function createBindings (connection) { 47 | connection.configureBindings(config.bindings, connection.name) 48 | .then( 49 | finish.bind(null, connection), 50 | onBindingError.bind(null, connection) 51 | ); 52 | } 53 | 54 | function finish (connection) { 55 | emit(connection.name + '.connection.configured', connection); 56 | resolve(); 57 | } 58 | 59 | this.addConnection(config.connection) 60 | .then( 61 | function (connection) { 62 | createExchanges(connection); 63 | return connection; 64 | }, 65 | reject 66 | ); 67 | }.bind(this)); 68 | return this.configuring[configName]; 69 | }; 70 | }; 71 | -------------------------------------------------------------------------------- /demo/pubsub/subscriber.js: -------------------------------------------------------------------------------- 1 | const rabbit = require('../../src/index.js'); 2 | 3 | const counts = { 4 | timeout: 0, // variable to hold the timeout 5 | started: 0, // variable to hold starting time 6 | received: 0, // variable to hold received count 7 | batch: 500, // expected batch size 8 | expected: 1000000 // expected message count 9 | }; 10 | 11 | // always setup your message handlers first 12 | 13 | // this handler will handle messages sent from the publisher 14 | rabbit.handle('publisher.message', function (msg) { 15 | // console.log( "Received:", JSON.stringify( msg.body ) ); 16 | // msg.ack(); 17 | if (counts.received % 5000 === 0) { 18 | report(); 19 | } 20 | if ((++counts.received) >= counts.expected - 1) { 21 | const diff = Date.now() - counts.started; 22 | console.log('Received', counts.received, 'messages after', diff, 'milliseconds'); 23 | } 24 | }); 25 | 26 | function report () { 27 | const diff = Date.now() - counts.started; 28 | console.log('Received', counts.received, 'messages after', diff, 'milliseconds'); 29 | } 30 | 31 | // it can make a lot of sense to share topology definition across 32 | // services that will be using the same topology to avoid 33 | // scenarios where you have race conditions around when 34 | // exchanges, queues or bindings are in place 35 | require('./topology.js')(rabbit, 'messages') 36 | .then(() => { 37 | notifyPublisher(); 38 | }); 39 | 40 | // now that our handlers are set up and topology is defined, 41 | // we can publish a request to let the publisher know we're up 42 | // and ready for messages. 43 | 44 | // because we will re-publish after a timeout, the messages will 45 | // expire if not picked up from the queue in time. 46 | // this prevents a bunch of requests from stacking up in the request 47 | // queue and causing the publisher to send multiple bundles 48 | let requestCount = 0; 49 | 50 | function notifyPublisher () { 51 | console.log('Sending request', ++requestCount); 52 | rabbit.request('wascally-pubsub-requests-x', { 53 | type: 'subscriber.request', 54 | replyTimeout: 15000, 55 | expiresAfter: 6000, 56 | routingKey: '', 57 | body: { ready: true, expected: counts.expected, batchSize: counts.batch } 58 | }).then(function (response) { 59 | // if we get a response, cancel any existing timeout 60 | counts.received = 0; 61 | counts.started = Date.now(); 62 | if (counts.timeout) { 63 | clearTimeout(counts.timeout); 64 | } 65 | console.log('Publisher replied.'); 66 | response.ack(); 67 | }); 68 | counts.timeout = setTimeout(notifyPublisher, 15000); 69 | } 70 | -------------------------------------------------------------------------------- /spec/integration/wildCardTypes.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | Demonstrates handling types based on wild card matching. 7 | Note that only 3 of four messages published match the pattern 8 | provided. 9 | */ 10 | 11 | function stallLongEnoughForBatchAckHandling () { 12 | return new Promise((resolve, reject) => { 13 | setTimeout(() => { 14 | resolve(); 15 | }, 500); 16 | }); 17 | } 18 | 19 | describe('Wild Card Type Handling', function () { 20 | let harness; 21 | 22 | before(function () { 23 | harness = harnessFactory(rabbit, () => {}, 3); 24 | harness.handle('#.a'); 25 | return rabbit.configure({ 26 | connection: config.connection, 27 | exchanges: [ 28 | { 29 | name: 'rabbot-ex.topic', 30 | type: 'topic', 31 | alternate: 'rabbot-ex.alternate', 32 | autoDelete: true 33 | } 34 | ], 35 | queues: [ 36 | { 37 | name: 'rabbot-q.topic', 38 | autoDelete: true, 39 | subscribe: true, 40 | deadletter: 'rabbot-ex.deadletter' 41 | } 42 | ], 43 | bindings: [ 44 | { 45 | exchange: 'rabbot-ex.topic', 46 | target: 'rabbot-q.topic', 47 | keys: 'this.is.*' 48 | } 49 | ] 50 | }) 51 | .then(() => 52 | Promise.all([ 53 | rabbit.publish('rabbot-ex.topic', { type: 'one.a', routingKey: 'this.is.one', body: 'one' }), 54 | rabbit.publish('rabbot-ex.topic', { type: 'two.i.a', routingKey: 'this.is.two', body: 'two' }), 55 | rabbit.publish('rabbot-ex.topic', { type: 'three-b.a', routingKey: 'this.is.three', body: 'three' }), 56 | rabbit.publish('rabbot-ex.topic', { type: 'a.four', routingKey: 'this.is.four', body: 'four' }) 57 | ]) 58 | ) 59 | .then(stallLongEnoughForBatchAckHandling); 60 | }); 61 | 62 | it('should handle all message types ending in "a"', function () { 63 | const results = harness.received.map((m) => 64 | ({ 65 | body: m.body, 66 | key: m.fields.routingKey 67 | }) 68 | ); 69 | sortBy(results, 'body').should.eql( 70 | [ 71 | { body: 'one', key: 'this.is.one' }, 72 | { body: 'three', key: 'this.is.three' }, 73 | { body: 'two', key: 'this.is.two' } 74 | ]); 75 | }); 76 | 77 | it("should not handle message types that don't match the pattern", function () { 78 | harness.unhandled.length.should.equal(1); 79 | harness.unhandled[0].body.should.eql('four'); 80 | }); 81 | 82 | after(function () { 83 | return harness.clean('default'); 84 | }); 85 | }); 86 | -------------------------------------------------------------------------------- /spec/integration/rejection.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | This spec demonstrates how a rejected message flows from 7 | the deadletter exchange (when configured) to the deadletter 8 | queue (when bound). 9 | 10 | You can easily break this by removing the binding between 11 | the deadletter exchange and deadletter queue (for example) 12 | */ 13 | describe('Rejecting Messages To A Deadletter', function () { 14 | let harness; 15 | 16 | before(function (done) { 17 | rabbit.configure({ 18 | connection: config.connection, 19 | exchanges: [ 20 | { 21 | name: 'rabbot-ex.topic', 22 | type: 'topic', 23 | alternate: 'rabbot-ex.alternate', 24 | autoDelete: true 25 | }, 26 | { 27 | name: 'rabbot-ex.deadletter', 28 | type: 'fanout', 29 | autoDelete: true 30 | } 31 | ], 32 | queues: [ 33 | { 34 | name: 'rabbot-q.topic', 35 | autoDelete: true, 36 | subscribe: true, 37 | deadletter: 'rabbot-ex.deadletter' 38 | }, 39 | { 40 | name: 'rabbot-q.deadletter', 41 | autoDelete: true, 42 | subscribe: true 43 | } 44 | ], 45 | bindings: [ 46 | { 47 | exchange: 'rabbot-ex.topic', 48 | target: 'rabbot-q.topic', 49 | keys: 'this.is.*' 50 | }, 51 | { 52 | exchange: 'rabbot-ex.deadletter', 53 | target: 'rabbot-q.deadletter', 54 | keys: [] 55 | } 56 | ] 57 | }).then(() => { 58 | harness = harnessFactory(rabbit, done, 2); 59 | harness.handlers.push( 60 | rabbit.handle('reject', (env) => { 61 | if (harness.received.length < 2) { 62 | env.reject(); 63 | } else { 64 | env.ack(); 65 | } 66 | harness.add(env); 67 | }) 68 | ); 69 | rabbit.publish( 70 | 'rabbot-ex.topic', 71 | { 72 | type: 'reject', 73 | routingKey: 'this.is.rejection', 74 | body: 'haters gonna hate' 75 | } 76 | ); 77 | }); 78 | }); 79 | 80 | it('should receive the message from bound queue and dead-letter queue', function () { 81 | const results = harness.received.map((m) => 82 | ({ 83 | body: m.body, 84 | key: m.fields.routingKey, 85 | exchange: m.fields.exchange 86 | }) 87 | ); 88 | results.should.eql( 89 | [ 90 | { body: 'haters gonna hate', key: 'this.is.rejection', exchange: 'rabbot-ex.topic' }, 91 | { body: 'haters gonna hate', key: 'this.is.rejection', exchange: 'rabbot-ex.deadletter' } 92 | ]); 93 | }); 94 | 95 | after(function () { 96 | return harness.clean('default'); 97 | }); 98 | }); 99 | -------------------------------------------------------------------------------- /spec/integration/badConnection.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | 4 | describe('Bad Connection', function () { 5 | const noop = () => {}; 6 | describe('when attempting a connection', function () { 7 | let error; 8 | before((done) => { 9 | rabbit.once('#.connection.failed', (err) => { 10 | error = err; 11 | done(); 12 | }); 13 | 14 | rabbit.addConnection({ 15 | name: 'silly', 16 | server: 'shfifty-five.gov', 17 | publishTimeout: 50, 18 | timeout: 100 19 | }).catch(noop); 20 | 21 | rabbit.addExchange({ name: 'silly-ex' }, 'silly').then(null, noop); 22 | }); 23 | 24 | it('should fail to connect', () => 25 | error.should.equal('No endpoints could be reached') 26 | ); 27 | 28 | it('should reject publish after timeout', () => 29 | rabbit.publish('silly-ex', { body: 'test' }, 'silly') 30 | .should.be.rejectedWith('No endpoints could be reached') 31 | ); 32 | 33 | after(() => rabbit.close('silly', true)); 34 | }); 35 | 36 | describe('when attempting a uri connection with an invalid password', function () { 37 | let error; 38 | before((done) => { 39 | rabbit.once('#.connection.failed', (err) => { 40 | error = err; 41 | done(); 42 | }); 43 | 44 | rabbit.addConnection({ 45 | name: 'noauthz', 46 | uri: 'amqp://guest:notguest@localhost:5672/%2f?heartbeat=10' 47 | }) 48 | .catch(noop); 49 | }); 50 | 51 | it('should fail to connect', () => { 52 | error.should.equal('No endpoints could be reached'); 53 | }); 54 | 55 | after(() => rabbit.close('noauthz', true)); 56 | }); 57 | 58 | describe('when configuring against a bad connection', function () { 59 | let config; 60 | before(() => { 61 | config = { 62 | connection: { 63 | name: 'silly2', 64 | server: 'this-is-not-a-real-thing-at-all.org', 65 | timeout: 100 66 | }, 67 | exchanges: [ 68 | { 69 | name: 'rabbot-ex.direct', 70 | type: 'direct', 71 | autoDelete: true 72 | } 73 | ], 74 | queues: [ 75 | { 76 | name: 'rabbot-q.direct', 77 | autoDelete: true, 78 | subscribe: true 79 | } 80 | ], 81 | bindings: [ 82 | { 83 | exchange: 'rabbot-ex.direct', 84 | target: 'rabbot-q.direct', 85 | keys: '' 86 | } 87 | ] 88 | }; 89 | }); 90 | 91 | it('should fail to connect', function () { 92 | return rabbit.configure(config) 93 | .should.be.rejectedWith('No endpoints could be reached'); 94 | }); 95 | 96 | after(function () { 97 | return rabbit.close('silly2', true); 98 | }); 99 | }); 100 | }); 101 | -------------------------------------------------------------------------------- /spec/integration/topicExchange.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | In this test it is worth noting the setup and the topics 7 | of the messages as they're sent as well as the queues of 8 | the messages in the test results. 9 | 10 | I set them up this way to demonstrate some more advanced 11 | routing techniques within rabbit and also test that all of 12 | this works when in use in rabbot. 13 | */ 14 | describe('Topic Exchange With Alternate Bindings', function () { 15 | let harness; 16 | 17 | before(function (done) { 18 | rabbit.configure({ 19 | connection: config.connection, 20 | exchanges: [ 21 | { 22 | name: 'rabbot-ex.topic', 23 | type: 'topic', 24 | alternate: 'rabbot-ex.alternate', 25 | autoDelete: true 26 | }, 27 | { 28 | name: 'rabbot-ex.alternate', 29 | type: 'fanout', 30 | autoDelete: true 31 | } 32 | ], 33 | queues: [ 34 | { 35 | name: 'rabbot-q.topic', 36 | autoDelete: true, 37 | subscribe: true, 38 | deadletter: 'rabbot-ex.deadletter' 39 | }, 40 | { 41 | name: 'rabbot-q.alternate', 42 | autoDelete: true, 43 | subscribe: true 44 | } 45 | ], 46 | bindings: [ 47 | { 48 | exchange: 'rabbot-ex.topic', 49 | target: 'rabbot-q.topic', 50 | keys: 'this.is.*' 51 | }, 52 | { 53 | exchange: 'rabbot-ex.alternate', 54 | target: 'rabbot-q.alternate', 55 | keys: [] 56 | } 57 | ] 58 | }).then(() => { 59 | // this message only arrives via the alternate 60 | rabbit.publish('rabbot-ex.topic', { type: 'topic', routingKey: 'this.is.a.test', body: 'broadcast' }); 61 | // this message is deliver by the topic route 62 | rabbit.publish('rabbot-ex.topic', { type: 'topic', routingKey: 'this.is.sparta', body: 'leonidas' }); 63 | // this message only arrives via the alternate 64 | rabbit.publish('rabbot-ex.topic', { type: 'topic', routingKey: 'a.test.this.is', body: 'yoda' }); 65 | }); 66 | 67 | harness = harnessFactory(rabbit, done, 3); 68 | harness.handle('topic'); 69 | }); 70 | 71 | it('should receive matched an unmatched topics due to alternate exchange', function () { 72 | const results = harness.received.map((m) => ({ 73 | body: m.body, 74 | key: m.fields.routingKey, 75 | queue: m.queue 76 | })); 77 | sortBy(results, 'body').should.eql( 78 | [ 79 | { body: 'broadcast', key: 'this.is.a.test', queue: 'rabbot-q.alternate' }, 80 | { body: 'leonidas', key: 'this.is.sparta', queue: 'rabbot-q.topic' }, 81 | { body: 'yoda', key: 'a.test.this.is', queue: 'rabbot-q.alternate' } 82 | ]); 83 | }); 84 | 85 | after(function () { 86 | return harness.clean('default'); 87 | }); 88 | }); 89 | -------------------------------------------------------------------------------- /spec/integration/consistentHash.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Consistent Hash Exchange', function () { 6 | let limit; 7 | let harness; 8 | 9 | before(function (done) { 10 | rabbit.configure({ 11 | connection: config.connection, 12 | exchanges: [ 13 | { 14 | name: 'rabbot-ex.consistent-hash', 15 | type: 'x-consistent-hash', 16 | autoDelete: true, 17 | arguments: { 18 | 'hash-header': 'CorrelationId' 19 | } 20 | } 21 | ], 22 | queues: [ 23 | { 24 | name: 'rabbot-q.hashed1', 25 | autoDelete: true, 26 | subscribe: true 27 | }, 28 | { 29 | name: 'rabbot-q.hashed2', 30 | autoDelete: true, 31 | subscribe: true 32 | }, 33 | { 34 | name: 'rabbot-q.hashed3', 35 | autoDelete: true, 36 | subscribe: true 37 | }, 38 | { 39 | name: 'rabbot-q.hashed4', 40 | autoDelete: true, 41 | subscribe: true 42 | } 43 | ], 44 | bindings: [ 45 | { 46 | exchange: 'rabbot-ex.consistent-hash', 47 | target: 'rabbot-q.hashed1', 48 | keys: '100' 49 | }, 50 | { 51 | exchange: 'rabbot-ex.consistent-hash', 52 | target: 'rabbot-q.hashed2', 53 | keys: '100' 54 | }, 55 | { 56 | exchange: 'rabbot-ex.consistent-hash', 57 | target: 'rabbot-q.hashed3', 58 | keys: '100' 59 | }, 60 | { 61 | exchange: 'rabbot-ex.consistent-hash', 62 | target: 'rabbot-q.hashed4', 63 | keys: '100' 64 | } 65 | ] 66 | }).then(() => { 67 | limit = 1000; 68 | harness = harnessFactory(rabbit, done, limit); 69 | harness.handle('balanced'); 70 | for (let i = 0; i < limit; i++) { 71 | rabbit.publish( 72 | 'rabbot-ex.consistent-hash', 73 | { 74 | type: 'balanced', 75 | correlationId: (i + i).toString(), 76 | body: 'message ' + i 77 | } 78 | ); 79 | } 80 | }); 81 | }); 82 | 83 | it('should distribute messages across queues within margin for error', function () { 84 | const consumers = harness.received.reduce((acc, m) => { 85 | const key = m.fields.consumerTag; 86 | if (acc[key]) { 87 | acc[key]++; 88 | } else { 89 | acc[key] = 1; 90 | } 91 | return acc; 92 | }, {}); 93 | 94 | const quarter = limit / 4; 95 | const margin = quarter / 4; 96 | const counts = Object.keys(consumers).map((k) => consumers[k]); 97 | counts.forEach((count) => { 98 | count.should.be.closeTo(quarter, margin); 99 | }); 100 | counts.reduce((acc, x) => acc + x, 0) 101 | .should.equal(limit); 102 | }); 103 | 104 | after(function () { 105 | return harness.clean('default'); 106 | }); 107 | }); 108 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "foo-foo-mq", 3 | "version": "9.2.0", 4 | "description": "Abstractions to simplify working with the RabbitMQ", 5 | "main": "src/index.js", 6 | "types": "src/index.d.ts", 7 | "engines": { 8 | "node": ">=16 <=22" 9 | }, 10 | "repository": "https://github.com/Foo-Foo-MQ/foo-foo-mq", 11 | "scripts": { 12 | "lint": "semistandard", 13 | "lint-fix": "semistandard --fix", 14 | "pretest": "semistandard", 15 | "test": "mocha --exit spec/**/*.spec.js", 16 | "commit": "git-cz", 17 | "coverage": "nyc --reporter=lcov --reporter=text npm test", 18 | "ci-coverage": "mkdir -p coverage && nyc npm test && nyc report --reporter=text-lcov > coverage/lcov.info", 19 | "release": "standard-version", 20 | "release:dry": "standard-version --dry-run", 21 | "run-container": "docker run -d --name foofoomq -p 15672:15672 -p 5672:5672 -v $(pwd)/rabbit_enabled_plugins:/etc/rabbitmq/enabled_plugins:ro rabbitmq:3-management", 22 | "start-container": "docker start foofoomq", 23 | "stop-container": "docker stop foofoomq", 24 | "remove-container": "docker rm -f foofoomq" 25 | }, 26 | "publishConfig": { 27 | "registry": "https://registry.npmjs.org/" 28 | }, 29 | "author": { 30 | "name": "Foo-Foo-MQ Team", 31 | "email": "dev@foofoomq.com" 32 | }, 33 | "contributors": [ 34 | { 35 | "name": "Zach Lintz", 36 | "email": "zlintz@foofoomq.com", 37 | "url": "https://github.com/zlintz" 38 | }, 39 | { 40 | "name": "Parker Johansen", 41 | "email": "auroq@foofoomq.com", 42 | "url": "https://github.com/auroq" 43 | }, 44 | { 45 | "name": "Alex Robson", 46 | "email": "asrobson@gmail.com", 47 | "url": "http://github.com/arobson" 48 | }, 49 | { 50 | "name": "Derick Bailey", 51 | "url": "http://derickbailey.com/" 52 | }, 53 | { 54 | "name": "Mario Kozjak", 55 | "url": "https://github.com/mkozjak" 56 | }, 57 | { 58 | "name": "Doug Neiner", 59 | "url": "http://code.dougneiner.com" 60 | }, 61 | { 62 | "name": "Brian Edgerton", 63 | "url": "https://github.com/brianedgerton" 64 | }, 65 | { 66 | "name": "Jim Cowart", 67 | "url": "http://github.com/ifandelse" 68 | }, 69 | { 70 | "name": "John Mathis", 71 | "url": "http://github.com/JohnDMathis" 72 | }, 73 | { 74 | "name": "Austin Young", 75 | "url": "http://github.com/LeankitAustin" 76 | } 77 | ], 78 | "config": { 79 | "commitizen": { 80 | "path": "cz-conventional-changelog" 81 | } 82 | }, 83 | "license": "MIT", 84 | "devDependencies": { 85 | "chai": "^4.3.4", 86 | "chai-as-promised": "^7.1.1", 87 | "commitizen": "^4.2.3", 88 | "coveralls": "^3.0.9", 89 | "cz-conventional-changelog": "^3.3.0", 90 | "lodash": "4.17.x", 91 | "mocha": "^10.0.0", 92 | "mocha-lcov-reporter": "^1.3.0", 93 | "nyc": "^15.1.0", 94 | "request": "^2.83.0", 95 | "semistandard": "^16.0.1", 96 | "sinon": "^14.0.0", 97 | "standard-version": "^9.1.1" 98 | }, 99 | "dependencies": { 100 | "amqplib": "~0.8.0", 101 | "bole": "^4.0.0", 102 | "debug": "^4.1.1", 103 | "machina": "^4.0.2", 104 | "node-monologue": "^2.0.0", 105 | "postal": "^2.0.5", 106 | "uuid": "^8.2.0" 107 | }, 108 | "semistandard": { 109 | "env": [ 110 | "mocha" 111 | ], 112 | "globals": [ 113 | "sinon", 114 | "should", 115 | "expect", 116 | "harnessFactory", 117 | "sortBy" 118 | ] 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | In the interest of fostering an open and welcoming environment, we as 6 | contributors and maintainers pledge to make participation in our project and 7 | our community a harassment-free experience for everyone, regardless of age, body 8 | size, disability, ethnicity, sex characteristics, gender identity and expression, 9 | level of experience, education, socio-economic status, nationality, personal 10 | appearance, race, religion, or sexual identity and orientation. 11 | 12 | ## Our Standards 13 | 14 | Examples of behavior that contributes to creating a positive environment 15 | include: 16 | 17 | * Using welcoming and inclusive language 18 | * Being respectful of differing viewpoints and experiences 19 | * Gracefully accepting constructive criticism 20 | * Focusing on what is best for the community 21 | * Showing empathy towards other community members 22 | 23 | Examples of unacceptable behavior by participants include: 24 | 25 | * The use of sexualized language or imagery and unwelcome sexual attention or 26 | advances 27 | * Trolling, insulting/derogatory comments, and personal or political attacks 28 | * Public or private harassment 29 | * Publishing others' private information, such as a physical or electronic 30 | address, without explicit permission 31 | * Other conduct which could reasonably be considered inappropriate in a 32 | professional setting 33 | 34 | ## Our Responsibilities 35 | 36 | Project maintainers are responsible for clarifying the standards of acceptable 37 | behavior and are expected to take appropriate and fair corrective action in 38 | response to any instances of unacceptable behavior. 39 | 40 | Project maintainers have the right and responsibility to remove, edit, or 41 | reject comments, commits, code, wiki edits, issues, and other contributions 42 | that are not aligned to this Code of Conduct, or to ban temporarily or 43 | permanently any contributor for other behaviors that they deem inappropriate, 44 | threatening, offensive, or harmful. 45 | 46 | ## Scope 47 | 48 | This Code of Conduct applies within all project spaces, and it also applies when 49 | an individual is representing the project or its community in public spaces. 50 | Examples of representing a project or community include using an official 51 | project e-mail address, posting via an official social media account, or acting 52 | as an appointed representative at an online or offline event. Representation of 53 | a project may be further defined and clarified by project maintainers. 54 | 55 | ## Enforcement 56 | 57 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 58 | reported by contacting the project team at lintz.zach@gmail.com. All 59 | complaints will be reviewed and investigated and will result in a response that 60 | is deemed necessary and appropriate to the circumstances. The project team is 61 | obligated to maintain confidentiality with regard to the reporter of an incident. 62 | Further details of specific enforcement policies may be posted separately. 63 | 64 | Project maintainers who do not follow or enforce the Code of Conduct in good 65 | faith may face temporary or permanent repercussions as determined by other 66 | members of the project's leadership. 67 | 68 | ## Attribution 69 | 70 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, 71 | available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html 72 | 73 | [homepage]: https://www.contributor-covenant.org 74 | [version]: https://www.contributor-covenant.org/version/1/4 75 | 76 | For answers to common questions about this code of conduct, see 77 | https://www.contributor-covenant.org/faq 78 | -------------------------------------------------------------------------------- /spec/integration/bulkPublish.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | Demonstrates how bulk publish API works 7 | in both formats. 8 | */ 9 | describe('Bulk Publish', function () { 10 | let harness; 11 | 12 | before(function (done) { 13 | this.timeout(10000); 14 | rabbit.configure({ 15 | connection: config.connection, 16 | exchanges: [ 17 | { 18 | name: 'rabbot-ex.direct-1', 19 | type: 'direct', 20 | autoDelete: true 21 | }, 22 | { 23 | name: 'rabbot-ex.direct-2', 24 | type: 'direct', 25 | autoDelete: true 26 | }, 27 | { 28 | name: 'rabbot-ex.direct-3', 29 | type: 'direct', 30 | autoDelete: true 31 | } 32 | ], 33 | queues: [ 34 | { 35 | name: 'rabbot-q.1', 36 | autoDelete: true, 37 | subscribe: true 38 | }, 39 | { 40 | name: 'rabbot-q.2', 41 | autoDelete: true, 42 | subscribe: true 43 | }, 44 | { 45 | name: 'rabbot-q.3', 46 | autoDelete: true, 47 | subscribe: true 48 | } 49 | ], 50 | bindings: [ 51 | { 52 | exchange: 'rabbot-ex.direct-1', 53 | target: 'rabbot-q.1', 54 | keys: '' 55 | }, 56 | { 57 | exchange: 'rabbot-ex.direct-2', 58 | target: 'rabbot-q.2', 59 | keys: '' 60 | }, 61 | { 62 | exchange: 'rabbot-ex.direct-3', 63 | target: 'rabbot-q.3', 64 | keys: '' 65 | } 66 | ] 67 | }); 68 | harness = harnessFactory(rabbit, done, 18); 69 | harness.handle('bulk'); 70 | rabbit.bulkPublish({ 71 | 'rabbot-ex.direct-1': [ 72 | { type: 'bulk', routingKey: '', body: 1 }, 73 | { type: 'bulk', routingKey: '', body: 2 }, 74 | { type: 'bulk', routingKey: '', body: 3 } 75 | ], 76 | 'rabbot-ex.direct-2': [ 77 | { type: 'bulk', routingKey: '', body: 4 }, 78 | { type: 'bulk', routingKey: '', body: 5 }, 79 | { type: 'bulk', routingKey: '', body: 6 } 80 | ], 81 | 'rabbot-ex.direct-3': [ 82 | { type: 'bulk', routingKey: '', body: 7 }, 83 | { type: 'bulk', routingKey: '', body: 8 }, 84 | { type: 'bulk', routingKey: '', body: 9 } 85 | ] 86 | }); 87 | 88 | rabbit.bulkPublish([ 89 | { type: 'bulk', routingKey: '', body: 10, exchange: 'rabbot-ex.direct-1' }, 90 | { type: 'bulk', routingKey: '', body: 11, exchange: 'rabbot-ex.direct-1' }, 91 | { type: 'bulk', routingKey: '', body: 12, exchange: 'rabbot-ex.direct-2' }, 92 | { type: 'bulk', routingKey: '', body: 13, exchange: 'rabbot-ex.direct-2' }, 93 | { type: 'bulk', routingKey: '', body: 14, exchange: 'rabbot-ex.direct-2' }, 94 | { type: 'bulk', routingKey: '', body: 15, exchange: 'rabbot-ex.direct-2' }, 95 | { type: 'bulk', routingKey: '', body: 16, exchange: 'rabbot-ex.direct-3' }, 96 | { type: 'bulk', routingKey: '', body: 17, exchange: 'rabbot-ex.direct-3' }, 97 | { type: 'bulk', routingKey: '', body: 18, exchange: 'rabbot-ex.direct-3' } 98 | ]); 99 | }); 100 | 101 | it('should bulk publish all messages successfully', function () { 102 | const results = harness.received.map((m) => ( 103 | parseInt(m.body) 104 | )); 105 | results.sort((a, b) => a - b).should.eql( 106 | [ 107 | 1, 108 | 2, 109 | 3, 110 | 4, 111 | 5, 112 | 6, 113 | 7, 114 | 8, 115 | 9, 116 | 10, 117 | 11, 118 | 12, 119 | 13, 120 | 14, 121 | 15, 122 | 16, 123 | 17, 124 | 18 125 | ]); 126 | }); 127 | 128 | after(function () { 129 | return harness.clean('default'); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /spec/behavior/ssl/ssl-connection.spec.js: -------------------------------------------------------------------------------- 1 | require('../../setup'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | const ampqlib = require('amqplib'); 5 | const rabbit = require('../../../src/index.js'); 6 | const config = require('../../integration/configuration'); 7 | 8 | describe('AMQP Connection', function () { 9 | describe('ssl support options when values are not paths', function () { 10 | let sandbox; 11 | let amqplibConnectSpy; 12 | 13 | before(function (done) { 14 | sandbox = sinon.createSandbox(); 15 | amqplibConnectSpy = sandbox.stub(ampqlib, 'connect').rejects(); 16 | rabbit.configure({ 17 | connection: { 18 | ...config.connection, 19 | retryLimit: 1, 20 | caPath: 'some-ca-value', 21 | certPath: 'cert', 22 | keyPath: 'key', 23 | pfxPath: 'pfx', 24 | passphrase: 'passphrase-is-not-a-path', 25 | port: null // To check the port is auto-resolved correctly to 5671 26 | } 27 | }) 28 | .catch(() => { 29 | done(); 30 | }); 31 | }); 32 | 33 | after(() => { 34 | sandbox.restore(); 35 | }); 36 | 37 | it('should have all of the provided ssl options', function () { 38 | const uri = 'amqps://guest:guest@127.0.0.1:5671/%2f?heartbeat=30'; 39 | 40 | const expectedConnectionOptions = { 41 | servername: '127.0.0.1', 42 | noDelay: true, 43 | timeout: 2000, 44 | cert: 'cert', 45 | key: 'key', 46 | pfx: 'pfx', 47 | passphrase: 'passphrase-is-not-a-path', 48 | ca: [ 49 | 'some-ca-value' 50 | ], 51 | clientProperties: { 52 | host: sinon.match.string, 53 | process: sinon.match.string, 54 | lib: sinon.match(/foo-foo-mq - .*/) 55 | } 56 | }; 57 | 58 | sinon.assert.callCount(amqplibConnectSpy, 1); 59 | sinon.assert.calledWith(amqplibConnectSpy, uri, expectedConnectionOptions); 60 | }); 61 | }); 62 | 63 | describe('ssl support options when values are paths', function () { 64 | let amqplibConnectSpy; 65 | let sandbox; 66 | const getLocalPath = (fileName) => path.join(__dirname, fileName); 67 | const sslPathSettings = [ 68 | getLocalPath('caPath1'), 69 | getLocalPath('caPath2'), 70 | getLocalPath('certPath'), 71 | getLocalPath('keyPath'), 72 | getLocalPath('pfxPath') 73 | ]; 74 | 75 | before(function (done) { 76 | sandbox = sinon.createSandbox(); 77 | sslPathSettings.forEach((settingName) => { 78 | fs.writeFileSync(settingName, `${settingName.split('/').pop()}-file-contents`); 79 | }); 80 | 81 | amqplibConnectSpy = sandbox.stub(ampqlib, 'connect').rejects(); 82 | rabbit.configure({ 83 | connection: { 84 | ...config.connection, 85 | retryLimit: 1, 86 | caPath: `${sslPathSettings[0]},${sslPathSettings[1]}`, 87 | certPath: sslPathSettings[2], 88 | keyPath: sslPathSettings[3], 89 | pfxPath: sslPathSettings[4], 90 | passphrase: 'passphrase-is-not-a-path' 91 | } 92 | }) 93 | .catch(() => { 94 | done(); 95 | }); 96 | }); 97 | 98 | after(() => { 99 | sslPathSettings.forEach((settingName) => { 100 | fs.unlinkSync(settingName); 101 | }); 102 | sandbox.restore(); 103 | }); 104 | 105 | it('should have all of the provided ssl options', function () { 106 | const uri = 'amqps://guest:guest@127.0.0.1:5672/%2f?heartbeat=30'; 107 | 108 | const expectedConnectionOptions = { 109 | servername: '127.0.0.1', 110 | noDelay: true, 111 | timeout: 2000, 112 | cert: Buffer.from('certPath-file-contents'), 113 | key: Buffer.from('keyPath-file-contents'), 114 | pfx: Buffer.from('pfxPath-file-contents'), 115 | passphrase: 'passphrase-is-not-a-path', 116 | ca: [ 117 | Buffer.from('caPath1-file-contents'), 118 | Buffer.from('caPath2-file-contents') 119 | ], 120 | clientProperties: { 121 | host: sinon.match.string, 122 | process: sinon.match.string, 123 | lib: sinon.match(/foo-foo-mq - .*/) 124 | } 125 | }; 126 | 127 | sinon.assert.callCount(amqplibConnectSpy, 1); 128 | sinon.assert.calledWith(amqplibConnectSpy, uri, expectedConnectionOptions); 129 | }); 130 | }); 131 | }); 132 | -------------------------------------------------------------------------------- /spec/integration/unhandled.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | describe('Unhandled Strategies', function () { 6 | /* 7 | This specification only works because the harness supplies 8 | a custom unhandled strategy for rabbot in `setup.js`: 9 | 10 | rabbit.onUnhandled( ( message ) => { 11 | unhandled.push( message ); 12 | message.ack(); 13 | check(); 14 | } ); 15 | 16 | This allows it to capture any unhandled messages as such 17 | and test accordingly. 18 | */ 19 | 20 | describe('Custom Strategy - Capturing Messages With No Handler', function () { 21 | let harness; 22 | 23 | before(function (done) { 24 | rabbit.configure({ 25 | connection: config.connection, 26 | exchanges: [ 27 | { 28 | name: 'rabbot-ex.direct', 29 | type: 'direct', 30 | autoDelete: true 31 | } 32 | ], 33 | queues: [ 34 | { 35 | name: 'rabbot-q.direct', 36 | autoDelete: true, 37 | subscribe: true 38 | } 39 | ], 40 | bindings: [ 41 | { 42 | exchange: 'rabbot-ex.direct', 43 | target: 'rabbot-q.direct', 44 | keys: [] 45 | } 46 | ] 47 | }).then(() => { 48 | rabbit.publish('rabbot-ex.direct', { type: 'junk', routingKey: '', body: 'uh oh' }); 49 | rabbit.publish('rabbot-ex.direct', { type: 'garbage', routingKey: '', body: 'uh oh' }); 50 | }); 51 | 52 | harness = harnessFactory(rabbit, done, 2); 53 | }); 54 | 55 | it('should capture all unhandled messages via custom unhandled message strategy', function () { 56 | const results = harness.unhandled.map((m) => ({ 57 | body: m.body, 58 | type: m.type 59 | })); 60 | sortBy(results, 'type').should.eql( 61 | [ 62 | { body: 'uh oh', type: 'garbage' }, 63 | { body: 'uh oh', type: 'junk' } 64 | ]); 65 | }); 66 | 67 | after(function () { 68 | return harness.clean('default'); 69 | }); 70 | }); 71 | 72 | /* 73 | This spec uses the `rejectUnhandled` strategy and demonstrates 74 | how one might reject unhandled messages to a catch-all queue 75 | via deadlettering for logging or processing. 76 | */ 77 | describe('Rejecting Unhandled Messages To A Deadletter', function () { 78 | let harness; 79 | 80 | before(function (done) { 81 | rabbit.configure({ 82 | connection: config.connection, 83 | exchanges: [ 84 | { 85 | name: 'rabbot-ex.topic', 86 | type: 'topic', 87 | alternate: 'rabbot-ex.alternate', 88 | autoDelete: true 89 | }, 90 | { 91 | name: 'rabbot-ex.deadletter', 92 | type: 'fanout', 93 | autoDelete: true 94 | } 95 | ], 96 | queues: [ 97 | { 98 | name: 'rabbot-q.topic', 99 | autoDelete: true, 100 | subscribe: true, 101 | deadletter: 'rabbot-ex.deadletter' 102 | }, 103 | { 104 | name: 'rabbot-q.deadletter', 105 | autoDelete: true, 106 | subscribe: true 107 | } 108 | ], 109 | bindings: [ 110 | { 111 | exchange: 'rabbot-ex.topic', 112 | target: 'rabbot-q.topic', 113 | keys: 'this.is.*' 114 | }, 115 | { 116 | exchange: 'rabbot-ex.deadletter', 117 | target: 'rabbot-q.deadletter', 118 | keys: [] 119 | } 120 | ] 121 | }).then(() => { 122 | harness = harnessFactory(rabbit, done, 1); 123 | rabbit.rejectUnhandled(); 124 | harness.handle({ queue: 'rabbot-q-deadletter' }); 125 | rabbit.publish( 126 | 'rabbot-ex.topic', 127 | { 128 | type: 'noonecares', 129 | routingKey: 'this.is.rejection', 130 | body: 'haters gonna hate' 131 | } 132 | ); 133 | }); 134 | }); 135 | 136 | it('should reject and then receive the message from dead-letter queue', function () { 137 | const results = harness.received.map((m) => 138 | ({ 139 | body: m.body, 140 | key: m.fields.routingKey, 141 | exchange: m.fields.exchange 142 | }) 143 | ); 144 | results.should.eql( 145 | [ 146 | { body: 'haters gonna hate', key: 'this.is.rejection', exchange: 'rabbot-ex.deadletter' } 147 | ]); 148 | }); 149 | 150 | after(function () { 151 | return harness.clean('default'); 152 | }); 153 | }); 154 | }); 155 | -------------------------------------------------------------------------------- /spec/behavior/publishLog.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup.js'); 2 | const _ = require('lodash'); 3 | const publishLog = require('../../src/publishLog'); 4 | 5 | describe('Publish log', function () { 6 | describe('when adding a message', function () { 7 | let log; 8 | const zero = {}; 9 | const one = {}; 10 | const two = {}; 11 | const three = {}; 12 | before(function () { 13 | log = publishLog(); 14 | log.add(zero); 15 | log.add(one); 16 | log.add(two); 17 | log.add(three); 18 | }); 19 | 20 | it('should keep a valid count', function () { 21 | log.count().should.equal(4); 22 | }); 23 | 24 | it('should assign sequence numbers correctly', function () { 25 | zero.sequenceNo.should.equal(0); 26 | one.sequenceNo.should.equal(1); 27 | two.sequenceNo.should.equal(2); 28 | three.sequenceNo.should.equal(3); 29 | }); 30 | }); 31 | 32 | describe('when removing a message', function () { 33 | let log; 34 | 35 | before(function () { 36 | log = publishLog(); 37 | log.add({}); 38 | log.add({}); 39 | log.add({}); 40 | log.add({}); 41 | log.add({}); 42 | }); 43 | 44 | describe('with valid sequence numbers', function () { 45 | let fourRemoved, zeroRemoved; 46 | before(function () { 47 | fourRemoved = log.remove(4); 48 | zeroRemoved = log.remove({ sequenceNo: 0 }); 49 | }); 50 | 51 | it('should return true when removing a message', function () { 52 | fourRemoved.should.equal(true); 53 | zeroRemoved.should.equal(true); 54 | }); 55 | 56 | it('should have removed two messages', function () { 57 | log.count().should.equal(3); 58 | }); 59 | 60 | describe('next message should get correct sequence', function () { 61 | let m; 62 | before(function () { 63 | m = {}; 64 | log.add(m); 65 | }); 66 | 67 | it('should assign sequence 5 to new message', function () { 68 | m.sequenceNo.should.equal(5); 69 | }); 70 | 71 | it('should increase count to 4', function () { 72 | log.count().should.equal(4); 73 | }); 74 | }); 75 | }); 76 | 77 | describe('with an invalid sequence number', function () { 78 | let removed; 79 | before(function () { 80 | removed = log.remove(10); 81 | }); 82 | 83 | it('should not decrease count', function () { 84 | log.count().should.equal(4); 85 | }); 86 | 87 | it('should return false when message is not in the log', function () { 88 | removed.should.equal(false); 89 | }); 90 | 91 | describe('next message should get correct sequence', function () { 92 | let m; 93 | before(function () { 94 | m = {}; 95 | log.add(m); 96 | }); 97 | 98 | it('should assign sequence 5 to new message', function () { 99 | m.sequenceNo.should.equal(6); 100 | }); 101 | 102 | it('should increase count to 5', function () { 103 | log.count().should.equal(5); 104 | }); 105 | }); 106 | }); 107 | }); 108 | 109 | describe('when resetting log', function () { 110 | let log; 111 | const zero = { id: 'zero' }; 112 | const one = { id: 'one' }; 113 | const two = { id: 'two' }; 114 | const three = { id: 'three' }; 115 | let list; 116 | before(function () { 117 | log = publishLog(); 118 | log.add(zero); 119 | log.add(one); 120 | log.add(two); 121 | log.add(three); 122 | list = log.reset(); 123 | }); 124 | 125 | it('should reset to 0 messages', function () { 126 | log.count().should.equal(0); 127 | }); 128 | 129 | it('should remove sequence numbers from messages', function () { 130 | should.not.exist(zero.sequenceNo); 131 | should.not.exist(one.sequenceNo); 132 | should.not.exist(two.sequenceNo); 133 | should.not.exist(three.sequenceNo); 134 | }); 135 | 136 | it('should remove sequence numbers from list', function () { 137 | _.each(list, function (m) { 138 | should.not.exist(m.sequenceNo); 139 | }); 140 | }); 141 | 142 | it('should return all messages', function () { 143 | list.should.eql([zero, one, two, three]); 144 | }); 145 | 146 | describe('when adding message to reset log', function () { 147 | let tmp; 148 | before(function () { 149 | tmp = {}; 150 | log.add(tmp); 151 | }); 152 | 153 | it('should start at index 0 when adding new message', function () { 154 | tmp.sequenceNo.should.equal(0); 155 | }); 156 | 157 | it('should only count new messages', function () { 158 | log.count().should.equal(1); 159 | }); 160 | }); 161 | }); 162 | }); 163 | -------------------------------------------------------------------------------- /spec/integration/queueSpecificHandle.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | This passes a queue name argument to rabbit's handle call 7 | so that it will register the harness's handler only for one 8 | of the bound fanout queue's. 9 | */ 10 | describe('Queue Specific Handler', function () { 11 | describe('with standard queues', function () { 12 | let harness; 13 | 14 | before(function (done) { 15 | rabbit.configure({ 16 | connection: config.connection, 17 | exchanges: [ 18 | { 19 | name: 'rabbot-ex.fanout', 20 | type: 'fanout', 21 | autoDelete: true 22 | } 23 | ], 24 | queues: [ 25 | { 26 | name: 'rabbot-q.general1', 27 | autoDelete: true, 28 | subscribe: true 29 | }, 30 | { 31 | name: 'rabbot-q.general2', 32 | noAck: true, 33 | autoDelete: true, 34 | subscribe: true 35 | } 36 | ], 37 | bindings: [ 38 | { 39 | exchange: 'rabbot-ex.fanout', 40 | target: 'rabbot-q.general1', 41 | keys: [] 42 | }, 43 | { 44 | exchange: 'rabbot-ex.fanout', 45 | target: 'rabbot-q.general2', 46 | keys: [] 47 | } 48 | ] 49 | }).then(() => { 50 | rabbit.publish('rabbot-ex.fanout', { type: '', routingKey: '', body: 'one' }); 51 | rabbit.publish('rabbot-ex.fanout', { type: '', routingKey: '', body: 'two' }); 52 | rabbit.publish('rabbot-ex.fanout', { type: '', routingKey: '', body: 'three' }); 53 | }); 54 | 55 | harness = harnessFactory(rabbit, done, 6); 56 | harness.handle('', undefined, 'rabbot-q.general1'); 57 | }); 58 | 59 | it('should only handle messages for the specified queue', function () { 60 | const results = harness.received.map((m) => ({ 61 | body: m.body, 62 | queue: m.queue 63 | })); 64 | sortBy(results, 'body').should.eql( 65 | [ 66 | { body: 'one', queue: 'rabbot-q.general1' }, 67 | { body: 'three', queue: 'rabbot-q.general1' }, 68 | { body: 'two', queue: 'rabbot-q.general1' } 69 | ]); 70 | }); 71 | 72 | it('should show the other messages as unhandled', function () { 73 | harness.unhandled.length.should.eql(3); 74 | }); 75 | 76 | after(function () { 77 | return harness.clean('default'); 78 | }); 79 | }); 80 | 81 | describe('with unique queue', function () { 82 | let harness; 83 | 84 | before(function (done) { 85 | rabbit.configure({ 86 | connection: config.connection, 87 | exchanges: [ 88 | { 89 | name: 'rabbot-ex.topic', 90 | type: 'topic', 91 | autoDelete: true 92 | } 93 | ], 94 | queues: [ 95 | { 96 | name: 'rabbot-q.general1', 97 | autoDelete: true, 98 | unique: 'hash', 99 | subscribe: true 100 | }, 101 | { 102 | name: 'rabbot-q.general2', 103 | noAck: true, 104 | autoDelete: true, 105 | subscribe: true 106 | } 107 | ], 108 | bindings: [ 109 | { 110 | exchange: 'rabbot-ex.topic', 111 | target: 'rabbot-q.general1', 112 | keys: ['a'] 113 | }, 114 | { 115 | exchange: 'rabbot-ex.topic', 116 | target: 'rabbot-q.general2', 117 | keys: ['b'] 118 | } 119 | ] 120 | }).then(() => { 121 | rabbit.publish('rabbot-ex.topic', { type: 'a', body: 'one' }); 122 | rabbit.publish('rabbot-ex.topic', { type: 'b', body: 'two' }); 123 | rabbit.publish('rabbot-ex.topic', { type: 'a', body: 'three' }); 124 | rabbit.publish('rabbot-ex.topic', { type: 'b', body: 'four' }); 125 | rabbit.publish('rabbot-ex.topic', { type: 'a', body: 'five' }); 126 | rabbit.publish('rabbot-ex.topic', { type: 'b', body: 'six' }); 127 | }); 128 | 129 | harness = harnessFactory(rabbit, done, 6); 130 | harness.handle('a', undefined, 'rabbot-q.general1'); 131 | }); 132 | 133 | it('should only handle messages for the specified queue', function () { 134 | const uniqueName = rabbit.getQueue('rabbot-q.general1').uniqueName; 135 | const results = harness.received.map((m) => ({ 136 | body: m.body, 137 | queue: m.queue 138 | })); 139 | sortBy(results, 'body').should.eql( 140 | [ 141 | { body: 'five', queue: uniqueName }, 142 | { body: 'one', queue: uniqueName }, 143 | { body: 'three', queue: uniqueName } 144 | ]); 145 | }); 146 | 147 | it('should show the other messages as unhandled', function () { 148 | harness.unhandled.length.should.eql(3); 149 | }); 150 | 151 | after(function () { 152 | return harness.clean('default'); 153 | }); 154 | }); 155 | }); 156 | -------------------------------------------------------------------------------- /HOW_TO_CONTRIBUTE.md: -------------------------------------------------------------------------------- 1 | ## Contributor Guide 2 | 3 | This is intended to help you make a contribution to Foo-foo-mq that is likely to land. 4 | It includes basic instructions for getting the project running locally, 5 | ensuring CI will pass, and reducing the chances that you will be asked to make changes by a maintainer 6 | or have the PR closed without comment for failing simple criteria. 7 | 8 | A lot of the criteria here is to make it much easier for maintainers to accept or reject PRs without having to do additional work. 9 | It's also intended to save people time and effort in advance if they're not interested in meeting the criteria required to submit an acceptable PR. 10 | 11 | ### Setup 12 | 13 | The current dev environment for Foo-foo-mq is: 14 | 15 | * Node 10+ 16 | * Docker (to run RabbitMQ in a container for integration tests) 17 | * [nvm] (recommended) 18 | 19 | It is recommended that you not use a RabbitMQ server running outside Docker 20 | as failing foo-foo-mq integration tests may make a mess that you'll be stuck cleaning up. :cry: 21 | It is much easier to just remove the Docker container and start over. 22 | 23 | ### NVM 24 | 25 | "[nvm] is a version manager for node.js." 26 | It is low maintenance way to ensure all contributors use the same version of node for development. 27 | 28 | The current minimum required version of node is specified in the [.nvmrc](.nvmrc) file. 29 | 30 | Assuming you have nvm installed, run the commands `nvm install` and `nvm use` 31 | to ensure you are on the right version of node before beginning development. 32 | 33 | 34 | #### Included Docker Container 35 | 36 | The repo includes a Dockerfile as well as npm commands to build, start and stop and remove the container: 37 | 38 | * `npm run run-container` - creates a container named `foofoomq` for tests 39 | * `npm run start-container` - starts the container if it was stopped 40 | * `npm run stop-container` - stops the container only 41 | * `npm run remove-container` - stops and removes the container entirely 42 | 43 | ### A Note About Features or Big Refactors 44 | 45 | There have been some occasions where a PR has conflicted with ongoing work, clashed with stated goals of the project 46 | or been a well-intended but undesired technology change. 47 | 48 | The maintainers understand that a lot of work goes into making a PR to someone else's project 49 | and would like to avoid having to close a PR and leave folks who'd like to contribute feeling as though their time and effort was not appreciated. 50 | 51 | Therefore, please open an issue or contact a maintainer before undertaking large scale effort along these lines. 52 | 53 | ### Commit Style 54 | 55 | `foo-foo-mq` uses [conventional commits] so that releases can be generated much more quickly 56 | (this includes automated generation of the CHANGELOG going forward). 57 | 58 | PRs with commits that do not follow this style will be asked to fix their commit log. 59 | PRs that do not conform and are not fixed will eventually just be closed. 60 | 61 | To make things easier, this repo is [![commitizen friendly]](http://commitizen.github.io/cz-cli/) 62 | 63 | Simply run `npm run commit` and it will prompt you through creating your commit message in the correct format. 64 | 65 | ### Running Tests & Coverage 66 | 67 | The expectation is for all PRs to include test coverage such that there is no drop in coverage. 68 | PRs that do not include changes to the spec folder are unlikely to be considered. 69 | A maintainer may ask you to add coverage. 70 | PRs that sit for long periods of time without tests are likely to be closed. 71 | 72 | To see the coverage before submitting a PR, you can run `npm run coverage` to get a full coverage report. 73 | 74 | #### New Features 75 | 76 | New features without both behavior and integration tests will not be accepted. 77 | The expectation is that features have tests that demonstrate your addition 78 | and will behave according to design during success and failure modes. 79 | 80 | #### Bug Fixes 81 | 82 | Bug fixes without additional tests are less likely to be accepted. 83 | The expectation is that if you are changing a behavior, 84 | you include a test to demonstrate that the correction addresses the behavior you are changing. 85 | 86 | This is very important as Foo-foo-mq has changed drastically over the years, 87 | and, without good tests in place, regressions are likely to creep in. 88 | Please help ensure that your fixes stay fixed :smile: 89 | 90 | ### Style & Linting 91 | 92 | Running `npm test`, `npm run lint` and `npm run lint-fix` are all methods to check/correct style/linting problems. 93 | The CI system also runs these and will fail a PR that violates the any rules. 94 | 95 | PRs that break or change style rules will be ignored, and if not repaired, they will be rejected. 96 | 97 | Foo-foo-mq now uses semistandard (standard + semicolons). 98 | This is not implying it is a perfect format. 99 | Rather it is a low maintenance way to have tooling and automation around ensuring a *consistent* style stay in place across all PRs. 100 | 101 | 102 | [nvm]: https://github.com/nvm-sh/nvm 103 | [conventional commits]: https://conventionalcommits.org/ 104 | [Commitizen friendly]: https://img.shields.io/badge/commitizen-friendly-brightgreen.svg 105 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # foo-foo-mq 2 | 3 | [![Build Status][travis-image]][travis-url] 4 | [![Coverage Status][coveralls-image]][coveralls-url] 5 | [![Version npm][version-image]][version-url] 6 | [![npm Downloads][downloads-image]][downloads-url] 7 | [![Dependencies][dependencies-image]][dependencies-url] 8 | [![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/) 9 | 10 | This is a very opinionated abstraction over amqplib to help simplify the implementation of several messaging patterns on RabbitMQ. 11 | 12 | > !Important! - successful use of this library will require a conceptual knowledge of AMQP and an understanding of RabbitMQ. 13 | 14 | ### Features: 15 | 16 | * Attempt to gracefully handle lost connections and channels 17 | * Automatically re-assert all topology on re-connection 18 | * Support the majority of RabbitMQ's extensions 19 | * Handle batching of acknowledgements and rejections 20 | * Topology & configuration via JSON (thanks to @JohnDMathis!) 21 | * Built-in support for JSON, binary and text message bodies 22 | * Support for custom serialization 23 | 24 | ### Assumptions & Defaults: 25 | 26 | * Fault-tolerance/resilience over throughput 27 | * Prefer "at least once delivery" 28 | * Default to publish confirmation 29 | * Default to ack mode on consumers 30 | * Heterogenous services that include statically typed languages 31 | * JSON as the default serialization provider for object based message bodies 32 | 33 | ## Documentation You Should Read 34 | 35 | * [Connection Management](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/docs/connections.md) - connection management 36 | * [Topology Setup](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/docs/topology.md) - topology configuration 37 | * [Publishing Guide](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/docs/publishing.md) - publishing and requesting 38 | * [Receiving Guide](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/docs/receiving.md) - subscribing and handling of messages 39 | * [Logging](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/docs/logging.md) - how foo-foo-mq logs 40 | 41 | ## Other Documents 42 | 43 | * [Contributor Guide](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/HOW_TO_CONTRIBUTE.md) 44 | * [Code of Conduct](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/CODE_OF_CONDUCT.md) 45 | * [Resources](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/RESOURCES.md) 46 | * [Maintainers](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/MAINTAINERS.md) 47 | * [Contributors](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/CONTRIBUTORS.md) 48 | * [Acknowledgements](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/ACKNOWLEDGEMENTS.md) 49 | * [Change Log](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/CHANGELOG.md) 50 | * [Differences From Wascally](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/docs/notwascally.md) 51 | 52 | ## Demos 53 | 54 | * [pubsub](https://github.com/Foo-Foo-MQ/foo-foo-mq/blob/master/demo/pubsub/README.md) 55 | 56 | ## API Example 57 | 58 | This contrived example is here to make it easy to see what the API looks like now that documentation is broken up across multiple pages. 59 | 60 | 61 | 62 | ```js 63 | const rabbit = require('foo-foo-mq'); 64 | 65 | rabbit.handle('MyMessage', (msg) => { 66 | console.log('received msg', msg.body); 67 | msg.ack(); 68 | }); 69 | 70 | rabbit.handle('MyRequest', (req) => { 71 | req.reply('yes?'); 72 | }); 73 | 74 | rabbit.configure({ 75 | connection: { 76 | name: 'default', 77 | user: 'guest', 78 | pass: 'guest', 79 | host: 'my-rabbitmq-server', 80 | port: 5672, 81 | vhost: '%2f', 82 | replyQueue: 'customReplyQueue' 83 | }, 84 | exchanges: [ 85 | { name: 'ex.1', type: 'fanout', autoDelete: true } 86 | ], 87 | queues: [ 88 | { name: 'q.1', autoDelete: true, subscribe: true }, 89 | ], 90 | bindings: [ 91 | { exchange: 'ex.1', target: 'q.1', keys: [] } 92 | ] 93 | }).then( 94 | () => console.log('connected!'); 95 | ); 96 | 97 | rabbit.request('ex.1', { type: 'MyRequest' }) 98 | .then( 99 | reply => { 100 | console.log('got response:', reply.body); 101 | reply.ack(); 102 | } 103 | ); 104 | 105 | rabbit.publish('ex.1', { type: 'MyMessage', body: 'hello!' }); 106 | 107 | 108 | setTimeout(() => { 109 | rabbit.shutdown(true) 110 | },5000); 111 | ``` 112 | 113 | ## Roadmap 114 | * improve support RabbitMQ backpressure mechanisms 115 | * add support for Rabbit's HTTP API 116 | 117 | [travis-image]: https://travis-ci.org/Foo-Foo-MQ/foo-foo-mq.svg?branch=master 118 | [travis-url]: https://travis-ci.org/Foo-Foo-MQ/foo-foo-mq 119 | [coveralls-url]: https://coveralls.io/github/Foo-Foo-MQ/foo-foo-mq?branch=master 120 | [coveralls-image]: https://coveralls.io/repos/github/Foo-Foo-MQ/foo-foo-mq/badge.svg?branch=master 121 | [version-image]: https://img.shields.io/npm/v/foo-foo-mq.svg?style=flat 122 | [version-url]: https://www.npmjs.com/package/foo-foo-mq 123 | [downloads-image]: https://img.shields.io/npm/dm/foo-foo-mq.svg?style=flat 124 | [downloads-url]: https://www.npmjs.com/package/foo-foo-mq 125 | [dependencies-image]: https://img.shields.io/david/Foo-Foo-MQ/foo-foo-mq.svg?style=flat 126 | [dependencies-url]: https://david-dm.org/Foo-Foo-MQ/foo-foo-mq 127 | -------------------------------------------------------------------------------- /src/amqp/exchange.js: -------------------------------------------------------------------------------- 1 | const defer = require('../defer'); 2 | const info = require('../info'); 3 | const exLog = require('../log.js')('rabbot.exchange'); 4 | const topLog = require('../log.js')('rabbot.topology'); 5 | const format = require('util').format; 6 | 7 | /* log 8 | * `rabbot.exchange` 9 | * `debug` 10 | * details for message publish - very verbose 11 | * `info` 12 | * `error` 13 | * no serializer is defined for message's content type 14 | * `rabbot.topology` 15 | * `info` 16 | * exchange declaration 17 | */ 18 | 19 | const DIRECT_REPLY_TO = 'amq.rabbitmq.reply-to'; 20 | const DIRECT_REGEX = /^rabbit(mq)?$/i; 21 | 22 | function aliasOptions (options, aliases, ...omit) { 23 | const keys = Object.keys(options); 24 | return keys.reduce((result, key) => { 25 | const alias = aliases[key] || key; 26 | if (omit.indexOf(key) < 0) { 27 | result[alias] = options[key]; 28 | } 29 | return result; 30 | }, {}); 31 | } 32 | 33 | function define (channel, options, connectionName) { 34 | const valid = aliasOptions(options, { 35 | alternate: 'alternateExchange' 36 | }, 'limit', 'persistent', 'publishTimeout'); 37 | topLog.info("Declaring %s exchange '%s' on connection '%s' with the options: %s", 38 | options.type, 39 | options.name, 40 | connectionName, 41 | JSON.stringify(valid) 42 | ); 43 | if (options.name === '') { 44 | return Promise.resolve(true); 45 | } else if (options.passive) { 46 | return channel.checkExchange(options.name); 47 | } else { 48 | return channel.assertExchange(options.name, options.type, valid); 49 | } 50 | } 51 | 52 | function getContentType (message) { 53 | if (message.contentType) { 54 | return message.contentType; 55 | } else if (typeof message.body === 'string') { 56 | return 'text/plain'; 57 | } else if (typeof message.body === 'object' && !Buffer.isBuffer(message.body)) { 58 | return 'application/json'; 59 | } else { 60 | return 'application/octet-stream'; 61 | } 62 | } 63 | 64 | function publish (channel, options, topology, log, serializers, message) { 65 | const channelName = options.name; 66 | const type = options.type; 67 | const baseHeaders = { 68 | CorrelationId: message.correlationId 69 | }; 70 | message.headers = Object.assign(baseHeaders, message.headers); 71 | const contentType = getContentType(message); 72 | const serializer = serializers[contentType]; 73 | if (!serializer) { 74 | const errMessage = format("Failed to publish message with contentType '%s' - no serializer defined", contentType); 75 | exLog.error(errMessage); 76 | return Promise.reject(new Error(errMessage)); 77 | } 78 | const payload = serializer.serialize(message.body); 79 | const publishOptions = { 80 | type: message.type || '', 81 | contentType: contentType, 82 | contentEncoding: 'utf8', 83 | correlationId: message.correlationId || '', 84 | replyTo: message.replyTo || topology.replyQueue.name || '', 85 | messageId: message.messageId || message.id || '', 86 | timestamp: message.timestamp || Date.now(), 87 | appId: message.appId || info.id, 88 | headers: message.headers || {}, 89 | expiration: message.expiresAfter || undefined, 90 | mandatory: message.mandatory || false 91 | }; 92 | if (publishOptions.replyTo === DIRECT_REPLY_TO || DIRECT_REGEX.test(publishOptions.replyTo)) { 93 | publishOptions.headers['direct-reply-to'] = 'true'; 94 | } 95 | if (!options.noConfirm && !message.sequenceNo) { 96 | log.add(message); 97 | } 98 | if (options.persistent || message.persistent) { 99 | publishOptions.persistent = true; 100 | } 101 | 102 | const effectiveKey = message.routingKey === '' ? '' : message.routingKey || publishOptions.type; 103 | exLog.debug("Publishing message ( type: '%s' topic: '%s', sequence: '%s', correlation: '%s', replyTo: '%s' ) to %s exchange '%s' on connection '%s'", 104 | publishOptions.type, 105 | effectiveKey, 106 | message.sequenceNo, 107 | publishOptions.correlationId, 108 | JSON.stringify(publishOptions), 109 | type, 110 | channelName, 111 | topology.connection.name); 112 | 113 | function onRejected (err) { 114 | log.remove(message); 115 | throw err; 116 | } 117 | 118 | function onConfirmed (sequence) { 119 | log.remove(message); 120 | return sequence; 121 | } 122 | 123 | if (options.noConfirm) { 124 | channel.publish( 125 | channelName, 126 | effectiveKey, 127 | payload, 128 | publishOptions 129 | ); 130 | return Promise.resolve(); 131 | } else { 132 | const deferred = defer(); 133 | const promise = deferred.promise; 134 | 135 | channel.publish( 136 | channelName, 137 | effectiveKey, 138 | payload, 139 | publishOptions, 140 | function (err, i) { 141 | if (err) { 142 | deferred.reject(err); 143 | } else { 144 | deferred.resolve(i); 145 | } 146 | } 147 | ); 148 | return promise 149 | .then(onConfirmed, onRejected); 150 | } 151 | } 152 | 153 | module.exports = function (options, topology, publishLog, serializers) { 154 | return topology.connection.getChannel(options.name, !options.noConfirm, 'exchange channel for ' + options.name) 155 | .then(function (channel) { 156 | return { 157 | channel: channel, 158 | define: define.bind(undefined, channel, options, topology.connection.name), 159 | release: function () { 160 | if (channel) { 161 | channel.release(); 162 | channel = undefined; 163 | } 164 | return Promise.resolve(true); 165 | }, 166 | publish: publish.bind(undefined, channel, options, topology, publishLog, serializers) 167 | }; 168 | }); 169 | }; 170 | -------------------------------------------------------------------------------- /spec/integration/request.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | function stallLongEnoughToARegisterUnhandleddMessages () { 6 | return new Promise((resolve, reject) => { 7 | setTimeout(() => { 8 | resolve(); 9 | }, 10); 10 | }); 11 | } 12 | 13 | describe('Request & Response', function () { 14 | let harness; 15 | before(function () { 16 | return rabbit.configure({ 17 | connection: config.connection, 18 | exchanges: [ 19 | { 20 | name: 'rabbot-ex.request', 21 | type: 'fanout', 22 | autoDelete: true 23 | } 24 | ], 25 | queues: [ 26 | { 27 | name: 'rabbot-q.request-1', 28 | autoDelete: true, 29 | subscribe: true 30 | }, 31 | { 32 | name: 'rabbot-q.request-2', 33 | autoDelete: true, 34 | subscribe: true 35 | }, 36 | { 37 | name: 'rabbot-q.request-3', 38 | autoDelete: true, 39 | subscribe: true 40 | }, 41 | { 42 | name: 'rabbot-q.request-4', 43 | autoDelete: true, 44 | subscribe: true 45 | }, 46 | { 47 | name: 'rabbot-q.request-5', 48 | autoDelete: true, 49 | subscribe: true 50 | } 51 | ], 52 | bindings: [ 53 | { 54 | exchange: 'rabbot-ex.request', 55 | target: 'rabbot-q.request-1', 56 | keys: [] 57 | }, 58 | { 59 | exchange: 'rabbot-ex.request', 60 | target: 'rabbot-q.request-2', 61 | keys: [] 62 | }, 63 | { 64 | exchange: 'rabbot-ex.request', 65 | target: 'rabbot-q.request-3', 66 | keys: [] 67 | }, 68 | { 69 | exchange: 'rabbot-ex.request', 70 | target: 'rabbot-q.request-4', 71 | keys: [] 72 | }, 73 | { 74 | exchange: 'rabbot-ex.request', 75 | target: 'rabbot-q.request-5', 76 | keys: [] 77 | } 78 | ] 79 | }); 80 | }); 81 | 82 | describe('when getting a response within the timeout', function () { 83 | let response1; 84 | let response2; 85 | let response3; 86 | 87 | before(function (done) { 88 | this.timeout(3000); 89 | harness = harnessFactory(rabbit, done, 21); 90 | 91 | harness.handle('polite', (q) => { 92 | q.reply(':D'); 93 | }, 'rabbot-q.request-1'); 94 | 95 | harness.handle('rude', (q) => { 96 | q.reply('>:@'); 97 | }, 'rabbot-q.request-1'); 98 | 99 | harness.handle('silly', (q) => { 100 | q.reply('...', { more: true }); 101 | q.reply('...', { more: true }); 102 | q.reply('...', { more: true }); 103 | setTimeout(() => q.reply('...'), 10); 104 | }, 'rabbot-q.request-1'); 105 | 106 | rabbit.request('rabbot-ex.request', { type: 'polite', body: 'how are you?' }) 107 | .then((response) => { 108 | response1 = response.body; 109 | harness.add(response); 110 | response.ack(); 111 | }); 112 | 113 | rabbit.request('rabbot-ex.request', { type: 'rude', body: 'why so dumb?' }) 114 | .then((response) => { 115 | response2 = response.body; 116 | harness.add(response); 117 | response.ack(); 118 | }); 119 | 120 | function onPart (part) { 121 | response3 = (response3 || '') + part.body; 122 | part.ack(); 123 | harness.add(part); 124 | } 125 | 126 | rabbit.request( 127 | 'rabbot-ex.request', 128 | { type: 'silly', body: 'do you like my yak-hair-shirt?' }, 129 | onPart 130 | ).then(onPart); 131 | }); 132 | 133 | it('should receive multiple responses', function () { 134 | const results = harness.received.map((m) => ({ 135 | body: m.body 136 | })); 137 | sortBy(results, 'body').should.eql( 138 | [ 139 | { body: '...' }, 140 | { body: '...' }, 141 | { body: '...' }, 142 | { body: '...' }, 143 | { body: ':D' }, 144 | { body: '>:@' }, 145 | { body: 'do you like my yak-hair-shirt?' }, 146 | { body: 'how are you?' }, 147 | { body: 'why so dumb?' } 148 | ]); 149 | }); 150 | 151 | it('should capture responses corresponding to the originating request', function () { 152 | response1.should.equal(':D'); 153 | response2.should.equal('>:@'); 154 | response3.should.equal('............'); 155 | }); 156 | 157 | after(function () { 158 | harness.clean(); 159 | }); 160 | }); 161 | 162 | describe('when performing scatter-gather', function () { 163 | const gather = []; 164 | before(function (done) { 165 | harness = harnessFactory(rabbit, () => {}, 4); 166 | let index = 0; 167 | harness.handle('scatter', (q) => { 168 | q.reply(`number: ${++index}`); 169 | }); 170 | 171 | function onReply (msg) { 172 | gather.push(msg); 173 | msg.ack(); 174 | } 175 | 176 | rabbit.request( 177 | 'rabbot-ex.request', 178 | { type: 'scatter', body: 'whatever', expect: 3 }, 179 | (msg) => { 180 | gather.push(msg); 181 | msg.ack(); 182 | } 183 | ) 184 | .then( 185 | onReply 186 | ) 187 | .then( 188 | stallLongEnoughToARegisterUnhandleddMessages 189 | ) 190 | .then(() => { 191 | done(); 192 | }); 193 | }); 194 | 195 | it('should have gathered desired replies', function () { 196 | gather.length.should.equal(3); 197 | }); 198 | 199 | it('should have ignored responses past limit', function () { 200 | harness.unhandled.length.should.equal(2); 201 | }); 202 | 203 | after(function () { 204 | harness.clean(); 205 | }); 206 | }); 207 | 208 | describe('when the request times out', function () { 209 | let timeoutError; 210 | const timeout = 100; 211 | before(function () { 212 | return rabbit.request( 213 | 'rabbot-ex.request', 214 | { type: 'polite', body: 'how are you?', replyTimeout: timeout } 215 | ) 216 | .then(null, (err) => { 217 | timeoutError = err; 218 | }); 219 | }); 220 | 221 | it('should receive rejection with timeout error', function () { 222 | timeoutError.message.should.eql(`No reply received within the configured timeout of ${timeout} ms`); 223 | }); 224 | }); 225 | 226 | after(function () { 227 | return harness.clean('default'); 228 | }); 229 | }); 230 | -------------------------------------------------------------------------------- /spec/behavior/configuration.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup.js'); 2 | 3 | describe('Configuration', function () { 4 | const noOp = function () {}; 5 | const connection = { 6 | name: 'test', 7 | configureBindings: noOp, 8 | configureExchanges: noOp, 9 | configureQueues: noOp, 10 | once: noOp 11 | }; 12 | const Broker = function (conn) { 13 | this.connection = conn; 14 | this.configurations = {}; 15 | this.configuring = {}; 16 | }; 17 | 18 | Broker.prototype.addConnection = function () { 19 | return Promise.resolve(this.connection); 20 | }; 21 | 22 | Broker.prototype.emit = function () {}; 23 | 24 | describe('with valid configuration', function () { 25 | const config = { 26 | exchanges: [{}], 27 | queues: [{}], 28 | bindings: [{}] 29 | }; 30 | let connectionMock; 31 | before(function () { 32 | connectionMock = sinon.mock(connection); 33 | connectionMock.expects('configureExchanges') 34 | .once() 35 | .withArgs(config.exchanges) 36 | .returns(Promise.resolve(true)); 37 | connectionMock.expects('configureQueues') 38 | .once() 39 | .withArgs(config.queues) 40 | .returns(Promise.resolve(true)); 41 | connectionMock.expects('configureBindings') 42 | .once() 43 | .withArgs(config.bindings, 'test') 44 | .returns(Promise.resolve(true)); 45 | require('../../src/config')(Broker); 46 | 47 | const broker = new Broker(connection); 48 | 49 | return broker.configure(config); 50 | }); 51 | 52 | it('should make expected calls', function () { 53 | connectionMock.verify(); 54 | }); 55 | 56 | after(function () { 57 | connectionMock.restore(); 58 | }); 59 | }); 60 | 61 | describe('with an initially failed connection', function () { 62 | const config = { 63 | exchanges: [{}], 64 | queues: [{}], 65 | bindings: [{}] 66 | }; 67 | let connectionMock; 68 | before(function () { 69 | connectionMock = sinon.mock(connection); 70 | connectionMock.expects('configureExchanges') 71 | .once() 72 | .withArgs(config.exchanges) 73 | .returns(Promise.resolve(true)); 74 | connectionMock.expects('configureQueues') 75 | .once() 76 | .withArgs(config.queues) 77 | .returns(Promise.resolve(true)); 78 | connectionMock.expects('configureBindings') 79 | .once() 80 | .withArgs(config.bindings, 'test') 81 | .returns(Promise.resolve(true)); 82 | require('../../src/config')(Broker); 83 | 84 | const broker = new Broker(connection); 85 | 86 | return broker.configure(config); 87 | }); 88 | 89 | it('should make expected calls', function () { 90 | connectionMock.verify(); 91 | }); 92 | 93 | after(function () { 94 | connectionMock.restore(); 95 | }); 96 | }); 97 | 98 | describe('when exchange creation fails', function () { 99 | const config = { 100 | exchanges: [{}], 101 | queues: [{}], 102 | bindings: [{}] 103 | }; 104 | let connectionMock; 105 | let error; 106 | before(function () { 107 | connectionMock = sinon.mock(connection); 108 | connectionMock.expects('configureExchanges') 109 | .once() 110 | .withArgs(config.exchanges) 111 | .returns(Promise.reject(new Error("Not feelin' it today"))); 112 | connectionMock.expects('configureQueues') 113 | .never(); 114 | connectionMock.expects('configureBindings') 115 | .never(); 116 | require('../../src/config')(Broker); 117 | 118 | const broker = new Broker(connection); 119 | 120 | return broker.configure(config) 121 | .then(null, function (err) { 122 | error = err; 123 | }); 124 | }); 125 | 126 | it('should make expected calls', function () { 127 | connectionMock.verify(); 128 | }); 129 | 130 | it('should return error', function () { 131 | error.toString().should.equal("Error: Not feelin' it today"); 132 | }); 133 | 134 | after(function () { 135 | connectionMock.restore(); 136 | }); 137 | }); 138 | 139 | describe('when queue creation fails', function () { 140 | const config = { 141 | exchanges: [{}], 142 | queues: [{}], 143 | bindings: [{}] 144 | }; 145 | let connectionMock; 146 | let error; 147 | before(function () { 148 | connectionMock = sinon.mock(connection); 149 | connectionMock.expects('configureExchanges') 150 | .once() 151 | .withArgs(config.exchanges) 152 | .returns(Promise.resolve(true)); 153 | connectionMock.expects('configureQueues') 154 | .once() 155 | .withArgs(config.queues) 156 | .returns(Promise.reject(new Error("Not feelin' it today"))); 157 | connectionMock.expects('configureBindings') 158 | .never(); 159 | require('../../src/config')(Broker); 160 | 161 | const broker = new Broker(connection); 162 | 163 | return broker.configure(config) 164 | .then(null, function (err) { 165 | error = err; 166 | }); 167 | }); 168 | 169 | it('should make expected calls', function () { 170 | connectionMock.verify(); 171 | }); 172 | 173 | it('should return error', function () { 174 | error.toString().should.equal("Error: Not feelin' it today"); 175 | }); 176 | 177 | after(function () { 178 | connectionMock.restore(); 179 | }); 180 | }); 181 | 182 | describe('when binding creation fails', function () { 183 | const config = { 184 | exchanges: [{}], 185 | queues: [{}], 186 | bindings: [{}] 187 | }; 188 | let connectionMock; 189 | let error; 190 | before(function () { 191 | connectionMock = sinon.mock(connection); 192 | connectionMock.expects('configureExchanges') 193 | .once() 194 | .withArgs(config.exchanges) 195 | .returns(Promise.resolve(true)); 196 | connectionMock.expects('configureQueues') 197 | .once() 198 | .withArgs(config.queues) 199 | .returns(Promise.resolve(true)); 200 | connectionMock.expects('configureBindings') 201 | .once() 202 | .withArgs(config.bindings, 'test') 203 | .returns(Promise.reject(new Error("Not feelin' it today"))); 204 | require('../../src/config')(Broker); 205 | 206 | const broker = new Broker(connection); 207 | 208 | return broker.configure(config) 209 | .then(null, function (err) { 210 | error = err; 211 | }); 212 | }); 213 | 214 | it('should make expected calls', function () { 215 | connectionMock.verify(); 216 | }); 217 | 218 | it('should return error', function () { 219 | error.toString().should.equal("Error: Not feelin' it today"); 220 | }); 221 | 222 | after(function () { 223 | connectionMock.restore(); 224 | }); 225 | }); 226 | }); 227 | -------------------------------------------------------------------------------- /spec/behavior/queue.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup.js'); 2 | const ampqQueue = require('../../src/amqp/queue'); 3 | 4 | describe('AMQP Queue', function () { 5 | let amqpChannelMock, options, topology, serializers; 6 | 7 | beforeEach(() => { 8 | amqpChannelMock = { 9 | ack: sinon.stub().callsFake(() => Promise.resolve()), 10 | nack: sinon.stub().callsFake(() => Promise.resolve()), 11 | checkQueue: sinon.stub().callsFake(() => Promise.resolve()), 12 | assertQueue: sinon.stub().callsFake(() => Promise.resolve()) 13 | }; 14 | 15 | options = { 16 | uniqueName: 'one-unique-name-coming-up' 17 | }; 18 | 19 | topology = { 20 | connection: { 21 | getChannel: sinon.stub().callsFake(() => Promise.resolve(amqpChannelMock)) 22 | } 23 | }; 24 | 25 | serializers = sinon.stub(); 26 | }); 27 | 28 | describe('when executing "define"', () => { 29 | describe('when options.passive is not set', () => { 30 | it('calls assertQueue', function () { 31 | return ampqQueue(options, topology, serializers) 32 | .then((instance) => { 33 | return instance.define(); 34 | }) 35 | .then(() => { 36 | amqpChannelMock.checkQueue.calledOnce.should.equal(false); 37 | amqpChannelMock.assertQueue.calledOnce.should.equal(true); 38 | }); 39 | }); 40 | }); 41 | 42 | describe('when options.passive is true', function () { 43 | it('calls checkQueue instead of assertQueue', () => { 44 | options.passive = true; 45 | return ampqQueue(options, topology, serializers) 46 | .then((instance) => { 47 | return instance.define(); 48 | }) 49 | .then(() => { 50 | amqpChannelMock.checkQueue.calledOnce.should.equal(true); 51 | amqpChannelMock.assertQueue.calledOnce.should.equal(false); 52 | }); 53 | }); 54 | }); 55 | 56 | describe('when options.type is not set', function () { 57 | it('sets `x-queue-type` to "classic"', () => { 58 | const qType = 'classic'; 59 | options.queueLimit = 1000; 60 | options.maxPriority = 100; 61 | return ampqQueue(options, topology, serializers) 62 | .then((instance) => { 63 | return instance.define(); 64 | }) 65 | .then(() => { 66 | amqpChannelMock.assertQueue.calledWith( 67 | options.uniqueName, 68 | { ...options, arguments: { 'x-queue-type': qType } }); 69 | }); 70 | }); 71 | }); 72 | 73 | describe('when options.type is "classic"', function () { 74 | it('sets `x-queue-type` to "classic" and respects deprecated fields', () => { 75 | options.type = 'classic'; 76 | options.queueLimit = 1000; 77 | options.autoDelete = 100; 78 | options.maxPriority = 100; 79 | return ampqQueue(options, topology, serializers) 80 | .then((instance) => { 81 | return instance.define(); 82 | }) 83 | .then(() => { 84 | amqpChannelMock.assertQueue.calledWith( 85 | options.uniqueName, 86 | { 87 | queueLimit: options.queueLimit, 88 | arguments: { 'x-queue-type': options.type } 89 | }); 90 | }); 91 | }); 92 | 93 | it('Omits `x-dead-letter-strategy` argument if given', () => { 94 | options.type = 'classic'; 95 | options.queueLimit = 1000; 96 | options.maxPriority = 100; 97 | options.deadLetterStrategy = 'at-least-once'; 98 | return ampqQueue(options, topology, serializers) 99 | .then((instance) => { 100 | return instance.define(); 101 | }) 102 | .then(() => { 103 | amqpChannelMock.assertQueue.calledWith( 104 | options.uniqueName, 105 | { 106 | ...options, 107 | arguments: { 108 | 'x-queue-type': options.type 109 | } 110 | }); 111 | }); 112 | }); 113 | 114 | it('Sets `x-queue-version` argument if given', () => { 115 | options.type = 'classic'; 116 | options.queueLimit = 1000; 117 | options.queueVersion = 2; 118 | options.maxPriority = 100; 119 | return ampqQueue(options, topology, serializers) 120 | .then((instance) => { 121 | return instance.define(); 122 | }) 123 | .then(() => { 124 | amqpChannelMock.assertQueue.calledWith( 125 | options.uniqueName, 126 | { 127 | ...options, 128 | arguments: { 129 | 'x-queue-type': options.type, 130 | 'x-queue-version': options.queueVersion 131 | } 132 | }); 133 | }); 134 | }); 135 | }); 136 | 137 | describe('when options.type is "quorum"', function () { 138 | it('sets `x-queue-type` to "quorum" and omits incompatible fields', () => { 139 | options.type = 'quorum'; 140 | options.queueLimit = 1000; 141 | options.autoDelete = true; 142 | options.maxPriority = 100; 143 | return ampqQueue(options, topology, serializers) 144 | .then((instance) => { 145 | return instance.define(); 146 | }) 147 | .then(() => { 148 | amqpChannelMock.assertQueue.calledWith( 149 | options.uniqueName, 150 | { 151 | queueLimit: options.queueLimit, 152 | arguments: { 'x-queue-type': options.type } 153 | }); 154 | }); 155 | }); 156 | 157 | it('sets `x-dead-letter-strategy` argument if given', () => { 158 | options.type = 'quorum'; 159 | options.queueLimit = 1000; 160 | options.autoDelete = true; 161 | options.maxPriority = 100; 162 | options.deadLetterStrategy = 'at-least-once'; 163 | return ampqQueue(options, topology, serializers) 164 | .then((instance) => { 165 | return instance.define(); 166 | }) 167 | .then(() => { 168 | amqpChannelMock.assertQueue.calledWith( 169 | options.uniqueName, 170 | { 171 | queueLimit: options.queueLimit, 172 | arguments: { 173 | 'x-queue-type': options.type, 174 | 'x-dead-letter-strategy': options.deadLetterStrategy 175 | } 176 | }); 177 | }); 178 | }); 179 | 180 | it('Omits `x-queue-version` argument if given', () => { 181 | options.type = 'classic'; 182 | options.queueLimit = 1000; 183 | options.queueVersion = 2; 184 | options.maxPriority = 100; 185 | return ampqQueue(options, topology, serializers) 186 | .then((instance) => { 187 | return instance.define(); 188 | }) 189 | .then(() => { 190 | amqpChannelMock.assertQueue.calledWith( 191 | options.uniqueName, 192 | { 193 | ...options, 194 | arguments: { 195 | 'x-queue-type': options.type 196 | } 197 | }); 198 | }); 199 | }); 200 | }); 201 | }); 202 | }); 203 | -------------------------------------------------------------------------------- /demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | es6: true, 4 | node: true 5 | }, 6 | extends: 'eslint:recommended', 7 | rules: { 8 | 'accessor-pairs': 'error', 9 | 'array-bracket-spacing': [ 10 | 'error', 11 | 'always' 12 | ], 13 | 'array-callback-return': 'error', 14 | 'arrow-body-style': 'error', 15 | 'arrow-parens': [ 16 | 'error', 17 | 'always' 18 | ], 19 | 'arrow-spacing': [ 20 | 'error', 21 | { 22 | after: true, 23 | before: true 24 | } 25 | ], 26 | 'block-scoped-var': 'error', 27 | 'block-spacing': 'error', 28 | 'brace-style': 'off', 29 | 'callback-return': 'off', 30 | camelcase: [ 31 | 'error', 32 | { 33 | properties: 'never' 34 | } 35 | ], 36 | 'class-methods-use-this': 'error', 37 | 'comma-dangle': 'error', 38 | 'comma-spacing': [ 39 | 'error', 40 | { 41 | after: true, 42 | before: false 43 | } 44 | ], 45 | 'comma-style': [ 46 | 'error', 47 | 'last' 48 | ], 49 | complexity: 'off', 50 | 'computed-property-spacing': 'off', 51 | 'consistent-return': 'off', 52 | 'consistent-this': 'off', 53 | curly: 'error', 54 | 'default-case': 'error', 55 | 'dot-location': [ 56 | 'error', 57 | 'property' 58 | ], 59 | 'dot-notation': 'off', 60 | 'eol-last': 'off', 61 | eqeqeq: 'error', 62 | 'func-call-spacing': 'error', 63 | 'func-names': [ 64 | 'error', 65 | 'never' 66 | ], 67 | 'func-style': 'off', 68 | 'generator-star-spacing': 'error', 69 | 'global-require': 'off', 70 | 'guard-for-in': 'error', 71 | 'handle-callback-err': 'off', 72 | 'id-blacklist': 'error', 73 | 'id-length': 'off', 74 | 'id-match': 'error', 75 | indent: 'off', 76 | 'init-declarations': 'off', 77 | 'jsx-quotes': 'error', 78 | 'key-spacing': 'off', 79 | 'keyword-spacing': 'off', 80 | 'linebreak-style': [ 81 | 'error', 82 | 'unix' 83 | ], 84 | 'lines-around-comment': 'error', 85 | 'max-depth': 'error', 86 | 'max-len': 'off', 87 | 'max-lines': 'off', 88 | 'max-nested-callbacks': 'error', 89 | 'max-params': 'off', 90 | 'max-statements': 'off', 91 | 'max-statements-per-line': 'error', 92 | 'multiline-ternary': [ 93 | 'error', 94 | 'never' 95 | ], 96 | 'new-cap': 'error', 97 | 'new-parens': 'error', 98 | 'newline-after-var': 'off', 99 | 'newline-before-return': 'off', 100 | 'newline-per-chained-call': 'off', 101 | 'no-alert': 'error', 102 | 'no-array-constructor': 'error', 103 | 'no-bitwise': 'error', 104 | 'no-caller': 'error', 105 | 'no-catch-shadow': 'error', 106 | 'no-confusing-arrow': 'error', 107 | 'no-continue': 'error', 108 | 'no-div-regex': 'error', 109 | 'no-duplicate-imports': 'error', 110 | 'no-else-return': 'off', 111 | 'no-console': 'off', 112 | 'no-empty': [ 113 | 'error', 114 | { 115 | allowEmptyCatch: true 116 | } 117 | ], 118 | 'no-empty-function': 'off', 119 | 'no-eq-null': 'error', 120 | 'no-eval': 'error', 121 | 'no-extend-native': 'error', 122 | 'no-extra-bind': 'off', 123 | 'no-extra-label': 'error', 124 | 'no-extra-parens': 'off', 125 | 'no-floating-decimal': 'error', 126 | 'no-global-assign': 'error', 127 | 'no-implicit-coercion': 'error', 128 | 'no-implicit-globals': 'error', 129 | 'no-implied-eval': 'error', 130 | 'no-inline-comments': 'off', 131 | 'no-inner-declarations': [ 132 | 'error', 133 | 'functions' 134 | ], 135 | 'no-invalid-this': 'error', 136 | 'no-iterator': 'error', 137 | 'no-label-var': 'error', 138 | 'no-labels': 'error', 139 | 'no-lone-blocks': 'error', 140 | 'no-lonely-if': 'error', 141 | 'no-loop-func': 'error', 142 | 'no-magic-numbers': 'off', 143 | 'no-mixed-operators': 'error', 144 | 'no-mixed-requires': 'error', 145 | 'no-multi-spaces': 'off', 146 | 'no-multi-str': 'error', 147 | 'no-multiple-empty-lines': 'error', 148 | 'no-negated-condition': 'off', 149 | 'no-nested-ternary': 'error', 150 | 'no-new': 'error', 151 | 'no-new-func': 'error', 152 | 'no-new-object': 'error', 153 | 'no-new-require': 'error', 154 | 'no-new-wrappers': 'error', 155 | 'no-octal-escape': 'error', 156 | 'no-param-reassign': 'off', 157 | 'no-path-concat': 'error', 158 | 'no-plusplus': 'off', 159 | 'no-process-env': 'error', 160 | 'no-process-exit': 'off', 161 | 'no-proto': 'error', 162 | 'no-prototype-builtins': 'error', 163 | 'no-restricted-globals': 'error', 164 | 'no-restricted-imports': 'error', 165 | 'no-restricted-modules': 'error', 166 | 'no-restricted-syntax': 'error', 167 | 'no-return-assign': 'error', 168 | 'no-script-url': 'error', 169 | 'no-self-compare': 'error', 170 | 'no-sequences': 'error', 171 | 'no-shadow': 'off', 172 | 'no-shadow-restricted-names': 'error', 173 | 'no-spaced-func': 'error', 174 | 'no-sync': 'off', 175 | 'no-tabs': 'off', 176 | 'no-template-curly-in-string': 'error', 177 | 'no-ternary': 'off', 178 | 'no-throw-literal': 'error', 179 | 'no-trailing-spaces': 'off', 180 | 'no-undef-init': 'error', 181 | 'no-undefined': 'off', 182 | 'no-underscore-dangle': 'off', 183 | 'no-unmodified-loop-condition': 'error', 184 | 'no-unneeded-ternary': 'off', 185 | 'no-unsafe-negation': 'error', 186 | 'no-unused-expressions': 'error', 187 | 'no-unused-vars': 'off', 188 | 'no-use-before-define': 'off', 189 | 'no-useless-call': 'error', 190 | 'no-useless-computed-key': 'error', 191 | 'no-useless-concat': 'off', 192 | 'no-useless-constructor': 'error', 193 | 'no-useless-escape': 'error', 194 | 'no-useless-rename': 'error', 195 | 'no-var': 'off', 196 | 'no-void': 'error', 197 | 'no-warning-comments': 'error', 198 | 'no-whitespace-before-property': 'error', 199 | 'no-with': 'error', 200 | 'object-curly-newline': 'off', 201 | 'object-curly-spacing': [ 202 | 'error', 203 | 'always' 204 | ], 205 | 'object-property-newline': [ 206 | 'error', 207 | { 208 | allowMultiplePropertiesPerLine: true 209 | } 210 | ], 211 | 'object-shorthand': 'off', 212 | 'one-var': 'off', 213 | 'one-var-declaration-per-line': [ 214 | 'error', 215 | 'initializations' 216 | ], 217 | 'operator-assignment': [ 218 | 'error', 219 | 'always' 220 | ], 221 | 'operator-linebreak': 'error', 222 | 'padded-blocks': 'off', 223 | 'prefer-arrow-callback': 'off', 224 | 'prefer-const': 'error', 225 | 'prefer-reflect': 'off', 226 | 'prefer-rest-params': 'off', 227 | 'prefer-spread': 'off', 228 | 'prefer-template': 'off', 229 | 'quote-props': 'off', 230 | quotes: 'off', 231 | radix: 'error', 232 | 'require-jsdoc': 'off', 233 | 'rest-spread-spacing': 'error', 234 | semi: 'off', 235 | 'semi-spacing': [ 236 | 'error', 237 | { 238 | after: true, 239 | before: false 240 | } 241 | ], 242 | 'sort-imports': 'error', 243 | 'sort-keys': 'off', 244 | 'sort-vars': 'off', 245 | 'space-before-blocks': 'error', 246 | 'space-before-function-paren': 'off', 247 | 'space-in-parens': 'off', 248 | 'space-infix-ops': 'error', 249 | 'space-unary-ops': 'error', 250 | 'spaced-comment': [ 251 | 'error', 252 | 'always' 253 | ], 254 | strict: [ 255 | 'error', 256 | 'never' 257 | ], 258 | 'symbol-description': 'error', 259 | 'template-curly-spacing': 'error', 260 | 'unicode-bom': [ 261 | 'error', 262 | 'never' 263 | ], 264 | 'valid-jsdoc': 'error', 265 | 'vars-on-top': 'off', 266 | 'wrap-iife': 'error', 267 | 'wrap-regex': 'error', 268 | 'yield-star-spacing': 'error', 269 | yoda: [ 270 | 'error', 271 | 'never' 272 | ] 273 | } 274 | }; 275 | -------------------------------------------------------------------------------- /spec/behavior/queueFsm.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup.js'); 2 | const _ = require('lodash'); 3 | const queueFsm = require('../../src/queueFsm'); 4 | const noOp = function () {}; 5 | const emitter = require('./emitter'); 6 | 7 | function channelFn (options) { 8 | const channel = { 9 | name: options.name, 10 | type: options.type, 11 | channel: emitter(), 12 | define: noOp, 13 | destroy: noOp, 14 | finalize: noOp, 15 | purge: noOp, 16 | release: noOp, 17 | getMessageCount: noOp, 18 | subscribe: noOp, 19 | unsubscribe: noOp 20 | }; 21 | const channelMock = sinon.mock(channel); 22 | 23 | return { 24 | mock: channelMock, 25 | factory: function () { 26 | return Promise.resolve(channel); 27 | } 28 | }; 29 | } 30 | 31 | describe('Queue FSM', function () { 32 | describe('when initialization fails', function () { 33 | let connection, topology, queue, channelMock, options, error; 34 | 35 | before(function (done) { 36 | options = { name: 'test', type: 'test' }; 37 | connection = emitter(); 38 | connection.addQueue = noOp; 39 | topology = emitter(); 40 | 41 | const ch = channelFn(options); 42 | channelMock = ch.mock; 43 | channelMock 44 | .expects('define') 45 | .once() 46 | .returns(Promise.reject(new Error('nope'))); 47 | 48 | queue = queueFsm(options, connection, topology, {}, ch.factory); 49 | queue.on('failed', function (err) { 50 | error = err; 51 | done(); 52 | }).once(); 53 | }); 54 | 55 | it('should have failed with an error', function () { 56 | error.toString().should.equal('Error: nope'); 57 | }); 58 | 59 | it('should be in failed state', function () { 60 | queue.state.should.equal('failed'); 61 | }); 62 | 63 | describe('when subscribing in failed state', function () { 64 | it('should reject subscribe with an error', function () { 65 | return queue.subscribe().should.be.rejectedWith(/nope/); 66 | }); 67 | }); 68 | 69 | describe('when purging in failed state', function () { 70 | it('should reject purge with an error', function () { 71 | return queue.purge().should.be.rejectedWith(/nope/); 72 | }); 73 | }); 74 | 75 | describe('when checking in failed state', function () { 76 | it('should reject check with an error', function () { 77 | return queue.check().should.be.rejectedWith(/nope/); 78 | }); 79 | }); 80 | }); 81 | 82 | describe('when initializing succeeds', function () { 83 | let connection, topology, queue, ch, channelMock, options, error; 84 | 85 | before(function (done) { 86 | options = { name: 'test', type: 'test' }; 87 | connection = emitter(); 88 | connection.addQueue = noOp; 89 | topology = emitter(); 90 | 91 | ch = channelFn(options); 92 | channelMock = ch.mock; 93 | channelMock 94 | .expects('define') 95 | .once() 96 | .resolves(true); 97 | 98 | queue = queueFsm(options, connection, topology, {}, ch.factory); 99 | queue.once('failed', function (err) { 100 | error = err; 101 | done(); 102 | }).once(); 103 | queue.once('defined', function () { 104 | done(); 105 | }).once(); 106 | }); 107 | 108 | it('should not have failed', function () { 109 | should.not.exist(error); 110 | }); 111 | 112 | it('should be in ready state', function () { 113 | queue.state.should.equal('ready'); 114 | }); 115 | 116 | describe('when subscribing in ready state', function () { 117 | before(function () { 118 | channelMock 119 | .expects('subscribe') 120 | .once() 121 | .resolves(true); 122 | }); 123 | 124 | it('should resolve subscribe without error', function () { 125 | queue.subscribe(); 126 | return queue.subscribe().should.be.fulfilled; 127 | }); 128 | 129 | it('should change options.subscribe to true', function () { 130 | options.subscribe.should.equal(true); 131 | }); 132 | 133 | it('should be in subscribed state', function () { 134 | queue.state.should.equal('subscribed'); 135 | }); 136 | }); 137 | 138 | describe('when purging in ready state', function () { 139 | before(function () { 140 | channelMock 141 | .expects('purge') 142 | .once() 143 | .resolves(10); 144 | 145 | channelMock 146 | .expects('subscribe') 147 | .once() 148 | .resolves(true); 149 | }); 150 | 151 | it('should resolve purge without error and resubscribe', function (done) { 152 | queue.on('subscribed', function () { 153 | queue.state.should.equal('subscribed'); 154 | done(); 155 | }); 156 | queue.purge().should.eventually.equal(10); 157 | }); 158 | }); 159 | 160 | describe('when checking after subscribed state', function () { 161 | it('should be in subscribed state', function () { 162 | return queue.state.should.equal('subscribed'); 163 | }); 164 | 165 | it('should resolve check without error', function () { 166 | return queue.check().should.be.fulfilled; 167 | }); 168 | }); 169 | 170 | describe('when unsubscribing', function () { 171 | before(function () { 172 | channelMock 173 | .expects('unsubscribe') 174 | .once() 175 | .resolves(true); 176 | }); 177 | 178 | it('should resolve unsubscribe without error', function () { 179 | return queue.unsubscribe().should.be.fulfilled; 180 | }); 181 | 182 | it('should change options.subscribe to false', function () { 183 | options.subscribe.should.equal(false); 184 | }); 185 | }); 186 | 187 | describe('when channel is closed remotely', function () { 188 | let channel; 189 | before(function (done) { 190 | channelMock 191 | .expects('define') 192 | .once() 193 | .resolves(); 194 | 195 | queue.once('defined', function () { 196 | done(); 197 | }); 198 | 199 | queue.once('closed', function () { 200 | queue.check(); 201 | }); 202 | 203 | ch.factory().then(function (q) { 204 | channel = q.channel; 205 | q.channel.raise('closed'); 206 | }); 207 | }); 208 | 209 | it('should reinitialize without error on check', function () { 210 | should.not.exist(error); 211 | }); 212 | 213 | it('should be in a ready state', function () { 214 | queue.state.should.equal('ready'); 215 | }); 216 | 217 | it('should not duplicate subscriptions to channel events', function () { 218 | _.each(channel.handlers, function (list, name) { 219 | list.length.should.equal(1); 220 | }); 221 | }); 222 | }); 223 | 224 | describe('when releasing', function () { 225 | before(function () { 226 | channelMock 227 | .expects('release') 228 | .once() 229 | .resolves(); 230 | 231 | return queue.release(); 232 | }); 233 | 234 | it('should remove handlers from topology and connection', function () { 235 | _.flatten(_.values(connection.handlers)).length.should.equal(0); 236 | _.flatten(_.values(topology.handlers)).length.should.equal(0); 237 | }); 238 | 239 | it('should release channel instance', function () { 240 | should.not.exist(queue.channel); 241 | }); 242 | 243 | describe('when checking a released queue', function () { 244 | it('should be released', function () { 245 | return queue.state.should.equal('released'); 246 | }); 247 | 248 | it('should reject check', function () { 249 | return queue.check().should.be.rejectedWith('Cannot establish queue \'test\' after intentionally closing its connection'); 250 | }); 251 | }); 252 | }); 253 | 254 | after(function () { 255 | connection.reset(); 256 | topology.reset(); 257 | channelMock.restore(); 258 | }); 259 | }); 260 | }); 261 | -------------------------------------------------------------------------------- /spec/integration/purgeQueue.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup'); 2 | const rabbit = require('../../src/index.js'); 3 | const config = require('./configuration'); 4 | 5 | /* 6 | Tests that queues are purged according to expected behavior: 7 | - auto-delete queues to NOT unsubscribed first 8 | - normal queues stop subscription first 9 | - after purge, subscription is restored 10 | - purging returns purged message count 11 | - purging does not break or disrupt channels 12 | */ 13 | describe('Purge Queue', function () { 14 | describe('when not subcribed', function () { 15 | before(function () { 16 | return rabbit.configure({ 17 | connection: config.connection, 18 | exchanges: [ 19 | { 20 | name: 'rabbot-ex.purged', 21 | type: 'topic', 22 | alternate: 'rabbot-ex.alternate', 23 | autoDelete: true 24 | } 25 | ], 26 | queues: [ 27 | { 28 | name: 'rabbot-q.purged', 29 | autoDelete: true, 30 | subscribe: false, 31 | deadletter: 'rabbot-ex.deadletter' 32 | } 33 | ], 34 | bindings: [ 35 | { 36 | exchange: 'rabbot-ex.purged', 37 | target: 'rabbot-q.purged', 38 | keys: 'this.is.#' 39 | } 40 | ] 41 | }) 42 | .then( 43 | () => 44 | Promise.all([ 45 | rabbit.publish('rabbot-ex.purged', { type: 'topic', routingKey: 'this.is.a.test', body: 'broadcast' }), 46 | rabbit.publish('rabbot-ex.purged', { type: 'topic', routingKey: 'this.is.sparta', body: 'leonidas' }), 47 | rabbit.publish('rabbot-ex.purged', { type: 'topic', routingKey: 'this.is.not.wine.wtf', body: 'socrates' }) 48 | ]) 49 | ); 50 | }); 51 | 52 | it('should have purged expected message count', function () { 53 | return rabbit.purgeQueue('rabbot-q.purged') 54 | .then( 55 | (purged) => { 56 | purged.should.equal(3); 57 | } 58 | ); 59 | }); 60 | 61 | it('should not re-subscribe to queue automatically (when not already subscribed)', function () { 62 | rabbit.getQueue('rabbot-q.purged') 63 | .state.should.equal('ready'); 64 | }); 65 | 66 | after(function () { 67 | return rabbit.deleteQueue('rabbot-q.purged') 68 | .then( 69 | () => rabbit.close('default', true) 70 | ); 71 | }); 72 | }); 73 | 74 | describe('when subcribed', function () { 75 | describe('and queue is autodelete', function () { 76 | let purgeCount; 77 | let harness; 78 | let handler; 79 | before(function (done) { 80 | rabbit.configure({ 81 | connection: config.connection, 82 | exchanges: [ 83 | { 84 | name: 'rabbot-ex.purged-2', 85 | type: 'topic', 86 | alternate: 'rabbot-ex.alternate', 87 | autoDelete: true 88 | } 89 | ], 90 | queues: [ 91 | { 92 | name: 'rabbot-q.purged-2', 93 | autoDelete: true, 94 | subscribe: true, 95 | limit: 1, 96 | deadletter: 'rabbot-ex.deadletter' 97 | } 98 | ], 99 | bindings: [ 100 | { 101 | exchange: 'rabbot-ex.purged-2', 102 | target: 'rabbot-q.purged-2', 103 | keys: 'this.is.#' 104 | } 105 | ] 106 | }) 107 | .then( 108 | () => { 109 | return Promise.all([ 110 | rabbit.publish('rabbot-ex.purged-2', { type: 'topic', routingKey: 'this.is.a.test', body: 'broadcast' }), 111 | rabbit.publish('rabbot-ex.purged-2', { type: 'topic', routingKey: 'this.is.sparta', body: 'leonidas' }), 112 | rabbit.publish('rabbot-ex.purged-2', { type: 'topic', routingKey: 'this.is.not.wine.wtf', body: 'socrates' }) 113 | ]); 114 | } 115 | ) 116 | .then( 117 | () => { 118 | return rabbit.purgeQueue('rabbot-q.purged-2') 119 | .then( 120 | count => { 121 | purgeCount = count; 122 | done(); 123 | } 124 | ); 125 | } 126 | ); 127 | harness = harnessFactory(rabbit, () => {}, 1); 128 | harness.handle('topic', (m) => { 129 | setTimeout(() => { 130 | m.ack(); 131 | }, 100); 132 | }); 133 | }); 134 | 135 | it('should have purged some messages', function () { 136 | purgeCount.should.be.greaterThan(0); 137 | (purgeCount + harness.received.length).should.eql(3); 138 | }); 139 | 140 | it('should re-subscribe to queue automatically (when not already subscribed)', function (done) { 141 | rabbit.getQueue('rabbot-q.purged-2') 142 | .state.should.equal('subscribed'); 143 | harness.clean(); 144 | handler = rabbit.handle('topic', (m) => { 145 | m.ack(); 146 | done(); 147 | }); 148 | rabbit.publish('rabbot-ex.purged-2', { type: 'topic', routingKey: 'this.is.easy', body: 'stapler' }); 149 | }); 150 | 151 | after(function () { 152 | return rabbit.deleteQueue('rabbot-q.purged-2') 153 | .then( 154 | () => { 155 | handler.remove(); 156 | return rabbit.close('default', true); 157 | } 158 | ); 159 | }); 160 | }); 161 | 162 | describe('and queue is not autodelete', function () { 163 | let purgeCount; 164 | let harness; 165 | let handler; 166 | before(function (done) { 167 | rabbit.configure({ 168 | connection: config.connection, 169 | exchanges: [ 170 | { 171 | name: 'rabbot-ex.purged-3', 172 | type: 'topic', 173 | alternate: 'rabbot-ex.alternate', 174 | autoDelete: true 175 | } 176 | ], 177 | queues: [ 178 | { 179 | name: 'rabbot-q.purged-3', 180 | autoDelete: false, 181 | subscribe: true, 182 | limit: 1, 183 | deadletter: 'rabbot-ex.deadletter' 184 | } 185 | ], 186 | bindings: [ 187 | { 188 | exchange: 'rabbot-ex.purged-3', 189 | target: 'rabbot-q.purged-3', 190 | keys: 'this.is.#' 191 | } 192 | ] 193 | }) 194 | .then( 195 | () => { 196 | return Promise.all([ 197 | rabbit.publish('rabbot-ex.purged-3', { type: 'topic', routingKey: 'this.is.a.test', body: 'broadcast' }), 198 | rabbit.publish('rabbot-ex.purged-3', { type: 'topic', routingKey: 'this.is.sparta', body: 'leonidas' }), 199 | rabbit.publish('rabbot-ex.purged-3', { type: 'topic', routingKey: 'this.is.not.wine.wtf', body: 'socrates' }) 200 | ]); 201 | } 202 | ) 203 | .then( 204 | () => { 205 | return rabbit.purgeQueue('rabbot-q.purged-3') 206 | .then( 207 | count => { 208 | purgeCount = count; 209 | done(); 210 | } 211 | ); 212 | } 213 | ); 214 | harness = harnessFactory(rabbit, () => {}, 1); 215 | harness.handle('topic', (m) => { 216 | setTimeout(() => { 217 | m.ack(); 218 | }, 100); 219 | }); 220 | }); 221 | 222 | it('should have purged some messages', function () { 223 | purgeCount.should.be.greaterThan(0); 224 | (purgeCount + harness.received.length).should.eql(3); 225 | }); 226 | 227 | it('should re-subscribe to queue automatically (when not already subscribed)', function (done) { 228 | rabbit.getQueue('rabbot-q.purged-3') 229 | .state.should.equal('subscribed'); 230 | harness.clean(); 231 | handler = rabbit.handle('topic', (m) => { 232 | m.ack(); 233 | done(); 234 | }); 235 | rabbit.publish('rabbot-ex.purged-3', { type: 'topic', routingKey: 'this.is.easy', body: 'stapler' }); 236 | }); 237 | 238 | after(function () { 239 | return rabbit.deleteQueue('rabbot-q.purged-3') 240 | .then( 241 | () => { 242 | handler.remove(); 243 | return rabbit.close('default', true); 244 | } 245 | ); 246 | }); 247 | }); 248 | }); 249 | }); 250 | -------------------------------------------------------------------------------- /src/amqp/connection.js: -------------------------------------------------------------------------------- 1 | const amqp = require('amqplib'); 2 | const fs = require('fs'); 3 | const AmqpConnection = require('amqplib/lib/callback_model').CallbackModel; 4 | const monad = require('./iomonad'); 5 | const log = require('../log')('rabbot.connection'); 6 | const info = require('../info'); 7 | const url = require('url'); 8 | const crypto = require('crypto'); 9 | const os = require('os'); 10 | 11 | /* log 12 | * `rabbot.amqp-connection` 13 | * `debug` 14 | * when amqplib's `connection.close` promise is rejected 15 | * `info` 16 | * connection attempt 17 | * connection success 18 | * connection failure 19 | * no reachable endpoints 20 | */ 21 | 22 | function getArgs (fn) { 23 | const fnString = fn.toString(); 24 | const argList = /[(]([^)]*)[)]/.exec(fnString)[1].split(','); 25 | return argList.map(String.prototype.trim); 26 | } 27 | 28 | function getInitialIndex (limit) { 29 | const pid = process.pid; 30 | let index = 0; 31 | if (pid <= limit) { 32 | const sha = crypto.createHash('sha1'); 33 | sha.write(os.hostname()); 34 | const buffer = sha.digest(); 35 | index = Math.abs(buffer.readInt32LE()) % limit; 36 | } else { 37 | index = pid % limit; 38 | } 39 | return index; 40 | } 41 | 42 | function getOption (opts, key, alt) { 43 | if (opts.get && supportsDefaults(opts.get)) { 44 | return opts.get(key, alt); 45 | } else { 46 | return opts[key] || alt; 47 | } 48 | } 49 | 50 | function getUri (protocol, user, pass, server, port, vhost, heartbeat, frameMax) { 51 | return `${protocol}://${user}:${pass}@${server}:${port}/${vhost}?heartbeat=${heartbeat}&frameMax=${frameMax}`; 52 | } 53 | 54 | function max (x, y) { 55 | return x > y ? x : y; 56 | } 57 | 58 | function parseUri (uri) { 59 | if (uri) { 60 | const parsed = new url.URL(uri); 61 | const heartbeat = parsed.searchParams.get('heartbeat'); 62 | const frameMax = parsed.searchParams.get('frameMax'); 63 | return { 64 | useSSL: parsed.protocol.startsWith('amqps'), 65 | user: decodeURIComponent(parsed.username), 66 | pass: decodeURIComponent(parsed.password), 67 | host: parsed.hostname, 68 | port: parsed.port, 69 | vhost: parsed.pathname ? parsed.pathname.slice(1) : undefined, 70 | heartbeat: heartbeat, 71 | frameMax: frameMax 72 | }; 73 | } 74 | } 75 | 76 | function split (x) { 77 | if (typeof x === 'number') { 78 | return [x]; 79 | } else if (Array.isArray(x)) { 80 | return x; 81 | } else { 82 | return x.split(',').map(trim); 83 | } 84 | } 85 | 86 | function supportsDefaults (opts) { 87 | return opts.get && getArgs(opts.get).length > 1; 88 | } 89 | 90 | function trim (x) { 91 | return x.trim(' '); 92 | } 93 | 94 | function readFromFileIfPathOrDefaultToInput (possiblePathOrValue) { 95 | return fs.existsSync(possiblePathOrValue) ? fs.readFileSync(possiblePathOrValue) : possiblePathOrValue; 96 | } 97 | 98 | const Adapter = function (parameters) { 99 | const uriOpts = parseUri(parameters.uri); 100 | Object.assign(parameters, uriOpts); 101 | const hosts = getOption(parameters, 'host'); 102 | const servers = getOption(parameters, 'server'); 103 | const brokers = getOption(parameters, 'RABBIT_BROKER'); 104 | const serverList = brokers || hosts || servers || 'localhost'; 105 | 106 | this.name = parameters ? (parameters.name || 'default') : 'default'; 107 | this.servers = split(serverList); 108 | this.connectionIndex = getInitialIndex(this.servers.length); 109 | this.heartbeat = getOption(parameters, 'RABBIT_HEARTBEAT') || getOption(parameters, 'heartbeat', 30); 110 | this.pass = getOption(parameters, 'RABBIT_PASSWORD') || getOption(parameters, 'pass', 'guest'); 111 | this.user = getOption(parameters, 'RABBIT_USER') || getOption(parameters, 'user', 'guest'); 112 | this.vhost = getOption(parameters, 'RABBIT_VHOST') || getOption(parameters, 'vhost', '%2f'); 113 | this.frameMax = getOption(parameters, 'RABBIT_FRAME_MAX') || getOption(parameters, 'frameMax', 4096); 114 | const timeout = getOption(parameters, 'RABBIT_TIMEOUT') || getOption(parameters, 'timeout', 2000); 115 | const certPath = getOption(parameters, 'RABBIT_CERT') || getOption(parameters, 'certPath'); 116 | const keyPath = getOption(parameters, 'RABBIT_KEY') || getOption(parameters, 'keyPath'); 117 | const caPaths = getOption(parameters, 'RABBIT_CA') || getOption(parameters, 'caPath'); 118 | const passphrase = getOption(parameters, 'RABBIT_PASSPHRASE') || getOption(parameters, 'passphrase'); 119 | const pfxPath = getOption(parameters, 'RABBIT_PFX') || getOption(parameters, 'pfxPath'); 120 | const useSSL = certPath || keyPath || passphrase || caPaths || pfxPath || parameters.useSSL; 121 | const portList = getOption(parameters, 'RABBIT_PORT') || getOption(parameters, 'port', (useSSL ? 5671 : 5672)); 122 | this.protocol = getOption(parameters, 'RABBIT_PROTOCOL') || getOption(parameters, 'protocol', undefined)?.replace(/:\/\/$/, '') || (useSSL ? 'amqps' : 'amqp'); 123 | this.ports = split(portList); 124 | this.options = { noDelay: true }; 125 | 126 | if (timeout) { 127 | this.options.timeout = timeout; 128 | } 129 | if (certPath) { 130 | this.options.cert = readFromFileIfPathOrDefaultToInput(certPath); 131 | } 132 | if (keyPath) { 133 | this.options.key = readFromFileIfPathOrDefaultToInput(keyPath); 134 | } 135 | if (passphrase) { 136 | this.options.passphrase = passphrase; 137 | } 138 | if (pfxPath) { 139 | this.options.pfx = readFromFileIfPathOrDefaultToInput(pfxPath); 140 | } 141 | if (caPaths) { 142 | const list = caPaths.split(','); 143 | this.options.ca = list.map(readFromFileIfPathOrDefaultToInput); 144 | } 145 | this.options.clientProperties = Object.assign({ 146 | host: info.host(), 147 | process: info.process(), 148 | lib: info.lib() 149 | }, parameters.clientProperties); 150 | this.limit = max(this.servers.length, this.ports.length); 151 | }; 152 | 153 | Adapter.prototype.connect = function () { 154 | return new Promise(function (resolve, reject) { 155 | const unreachable = 'No endpoints could be reached'; 156 | const attempted = []; 157 | const attempt = function () { 158 | const [nextUri, serverHostname] = this.getNextUri(); 159 | log.info("Attempting connection to '%s' (%s)", this.name, nextUri); 160 | function onConnection (connection) { 161 | connection.uri = nextUri; 162 | log.info("Connected to '%s' (%s)", this.name, nextUri); 163 | resolve(connection); 164 | } 165 | function onConnectionError (err) { 166 | log.info("Failed to connect to '%s' (%s) with, '%s'", this.name, nextUri, err); 167 | attempted.push(nextUri); 168 | this.bumpIndex(); 169 | if (attempted.length < this.limit) { 170 | attempt(err); 171 | } else { 172 | log.info('Cannot connect to `%s` - all endpoints failed', this.name); 173 | reject(unreachable); 174 | } 175 | } 176 | if (attempted.indexOf(nextUri) < 0) { 177 | amqp.connect(nextUri, Object.assign({ servername: serverHostname }, this.options)) 178 | .then(onConnection.bind(this), onConnectionError.bind(this)); 179 | } else { 180 | log.info('Cannot connect to `%s` - all endpoints failed', this.name); 181 | reject(unreachable); 182 | } 183 | }.bind(this); 184 | attempt(); 185 | }.bind(this)); 186 | }; 187 | 188 | Adapter.prototype.bumpIndex = function () { 189 | if (this.limit - 1 > this.connectionIndex) { 190 | this.connectionIndex++; 191 | } else { 192 | this.connectionIndex = 0; 193 | } 194 | }; 195 | 196 | Adapter.prototype.getNextUri = function () { 197 | const server = this.getNext(this.servers); 198 | const port = this.getNext(this.ports); 199 | const uri = getUri(this.protocol, encodeURIComponent(this.user), encodeURIComponent(this.pass), server, port, this.vhost, this.heartbeat, this.frameMax); 200 | return [uri, server]; 201 | }; 202 | 203 | Adapter.prototype.getNext = function (list) { 204 | if (this.connectionIndex >= list.length) { 205 | return list[0]; 206 | } else { 207 | return list[this.connectionIndex]; 208 | } 209 | }; 210 | 211 | module.exports = function (options) { 212 | const close = function (connection) { 213 | connection.close() 214 | .then(null, function (err) { 215 | // for some reason calling close always gets a rejected promise 216 | // I can't imagine a good reason for this, so I'm basically 217 | // only showing this at the debug level 218 | log.debug(`Error was reported during close of connection '${options.name}' - '${err}'`); 219 | }); 220 | }; 221 | const adapter = new Adapter(options); 222 | return monad(options, 'connection', adapter.connect.bind(adapter), AmqpConnection, close); 223 | }; 224 | -------------------------------------------------------------------------------- /src/index.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | export = Broker; 4 | 5 | declare namespace Broker { 6 | // Connection management 7 | export function addConnection(options: ConnectionOptions): void; 8 | export function close( 9 | connectionName?: string, 10 | reset?: boolean 11 | ): Promise; 12 | export function closeAll(reset?: boolean): Promise; 13 | export function retry(): Promise; 14 | export function shutdown(): Promise; 15 | 16 | // Managing topology 17 | export function configure(options: ConfigurationOptions): Promise; 18 | export function addExchange( 19 | exchangeName: string, 20 | exchangeType: ExchangeType, 21 | options?: Omit, 22 | connectionName?: string 23 | ): Promise; 24 | export function addQueue( 25 | queueName: string, 26 | options?: Omit, 27 | connectionName?: string 28 | ): Promise; 29 | export function bindExchange( 30 | sourceExchange: string, 31 | targetExchange: string, 32 | routingKeys?: string, 33 | connectionName?: string 34 | ): Promise; 35 | export function bindQueue( 36 | sourceExchange: string, 37 | targetQueue: string, 38 | routingKeys?: string | string[], 39 | connectionName?: string 40 | ): Promise; 41 | export function purgeQueue( 42 | queueName: string, 43 | connectionName?: string 44 | ): Promise; 45 | 46 | // Publishing 47 | export function publish( 48 | exchangeName: string, 49 | options: PublishOptions, 50 | connectionName?: string 51 | ): Promise; 52 | export function request( 53 | exchangeName: string, 54 | options: PublishOptions, 55 | connectionName?: string 56 | ): Promise>; 57 | export function bulkPublish( 58 | set: 59 | | BulkPublishSet 60 | | Array>, 61 | connectionName?: string 62 | ): Promise; 63 | 64 | // Receiving 65 | export function handle( 66 | options: HandlerOptions, 67 | handler: (message: Message) => any 68 | ): Promise; 69 | export function handle( 70 | typeName: string, 71 | handler: (message: Message) => any, 72 | queueName?: string, 73 | context?: string 74 | ): Promise; 75 | export function startSubscription( 76 | queueName: string, 77 | exclusive?: boolean, 78 | connectionName?: string 79 | ): void; 80 | export function stopSubscription(): void; 81 | 82 | // Custom serializers 83 | export function serialize(object: any): Buffer; 84 | export function deserialize(bytes: Buffer, encoding: string): any; 85 | export function addSerializer( 86 | contentType: string, 87 | serializer: { 88 | deserialize: (bytes: Buffer, encoding: string) => any; 89 | serialize: (object: any) => any; 90 | } 91 | ): void; 92 | 93 | // Event handler 94 | export function on(event: string, handler: (...args: any[]) => void): any; 95 | /** Remove all event subscriptions */ 96 | export function off(): void; 97 | /** Emit an event*/ 98 | export function emit(topic: string, data?: any, timestamp?: Date): void; 99 | 100 | // Unhandled messages 101 | export function onUnhandled(handler: (msg: Message) => void): void; 102 | export function nackUnhandled(handler: (msg: Message) => void): void; 103 | export function rejectUnhandled(handler: (msg: Message) => void): void; 104 | export function onReturned(handler: (msg: Message) => void): void; 105 | 106 | // Undocumented 107 | export function reset(): void; 108 | export function setAckInterval(interval: number): void; 109 | export function clearAckInterval(): void; 110 | export function nackOnError(): void; 111 | export function ignoreHandlerErrors(): void; 112 | export function getExchange(name: string, connectionName?: string): any; 113 | export function batchAck(): void; 114 | export function unbindExchange( 115 | source: string, 116 | target: string, 117 | keys: string | string[], 118 | connectionName?: string 119 | ): Promise; 120 | export function unbindQueue( 121 | source: string, 122 | target: string, 123 | keys: string | string[], 124 | connectionName?: string 125 | ): Promise; 126 | 127 | export function log( 128 | loggers: Array<{ 129 | level: string; 130 | stream: { 131 | write(data: string): void; 132 | }; 133 | }> 134 | ): void; 135 | 136 | export const connections: Record; 137 | 138 | export interface Message { 139 | ack(): Promise; 140 | nack(): Promise; 141 | reject(): Promise; 142 | reply( 143 | message: ReplyBodyType, 144 | options?: { 145 | more: string; 146 | replyType: string; 147 | contentType: string; 148 | headers: { 149 | [key: string]: string; 150 | }; 151 | } 152 | ): Promise; 153 | fields: MessageFields; 154 | properties: MessageProperties; 155 | body: BodyType; 156 | content: { 157 | type: string; 158 | data: Buffer; 159 | }; 160 | type: string; 161 | quarantine: boolean; 162 | } 163 | 164 | export interface MessageFields { 165 | consumerTag: string; 166 | deliveryTag: string; 167 | redelivered: boolean; 168 | exchange: string; 169 | routingKey: string; 170 | } 171 | 172 | export interface MessageProperties { 173 | contentType: string; 174 | contentEncoding: string; 175 | headers: { 176 | [key: string]: any; 177 | }; 178 | correlationId: string; 179 | replyTo: string; 180 | messageId: string; 181 | type: string; 182 | appId: string; 183 | } 184 | 185 | export type ExchangeType = "fanout" | "topic" | "direct"; 186 | 187 | export interface ConfigurationOptions { 188 | connection: ConnectionOptions; 189 | exchanges?: Array; 190 | queues?: Array; 191 | bindings?: Array; 192 | } 193 | 194 | export interface ConnectionOptions { 195 | uri?: string; 196 | name?: string; 197 | host?: string; 198 | port?: number; 199 | server?: string | string[]; 200 | vhost?: string; 201 | protocol?: "amqp" | "amqps"; 202 | user?: string; 203 | pass?: string; 204 | timeout?: number; 205 | heartbeat?: number; 206 | frameMax?: number; 207 | replyQueue?: 208 | | boolean 209 | | string 210 | | { 211 | name: string; 212 | autoDelete?: boolean; 213 | subscribe?: boolean; 214 | }; 215 | publishTimeout?: number; 216 | replyTimeout?: number; 217 | failAfter?: number; 218 | retryLimit?: number; 219 | waitMin?: number; 220 | waitMax?: number; 221 | waitIncrement?: number; 222 | clientProperties?: any; 223 | caPath?: string; 224 | certPath?: string; 225 | keyPath?: string; 226 | passphrase?: string; 227 | pfxPath?: string; 228 | } 229 | 230 | export interface QueueOptions { 231 | name: string; 232 | limit?: number; 233 | queueLimit?: number; 234 | queueVersion?: 1 | 2; 235 | deadLetter?: string; 236 | deadLetterRoutingKey?: string; 237 | deadLetterStrategy?: "at-most-once" | "at-least-once"; 238 | subscribe?: boolean; 239 | autoDelete?: boolean; 240 | passive?: boolean; 241 | messageTtl?: number; 242 | type?: "classic" | "quorum"; 243 | overflow?: "drop-head" | "reject-publish"; 244 | } 245 | 246 | export interface BindingOptions { 247 | exchange: string; 248 | target: string; 249 | keys?: string | string[]; 250 | } 251 | 252 | export interface ExchangeOptions { 253 | name: string; 254 | type: ExchangeType; 255 | publishTimeout?: number; 256 | alternate?: string; 257 | persistent?: boolean; 258 | durable?: boolean; 259 | } 260 | 261 | export interface PublishOptions { 262 | routingKey?: string; 263 | type?: string; 264 | correlationId?: string; 265 | contentType?: string; 266 | body?: MessageBodyType; 267 | messageId?: string; 268 | expiresAfter?: number; 269 | timestamp?: number; 270 | mandatory?: boolean; 271 | persistent?: boolean; 272 | headers?: { 273 | [key: string]: string; 274 | }; 275 | timeout?: number; 276 | } 277 | 278 | export interface BulkPublishSet { 279 | [exchangeName: string]: Array>; 280 | } 281 | 282 | export interface HandlerOptions { 283 | queue: string; 284 | type: string; 285 | autoNack?: boolean; 286 | context?: any; 287 | handler?(msg: Message): any; 288 | } 289 | 290 | export interface Handler { 291 | (msg: Message): Promise; 292 | remove(): void; 293 | catch(errorHandler: (err: any, msg: Message) => void): void; 294 | } 295 | } 296 | -------------------------------------------------------------------------------- /src/ackBatch.js: -------------------------------------------------------------------------------- 1 | const postal = require('postal'); 2 | const Monologue = require('node-monologue'); 3 | const signal = postal.channel('rabbit.ack'); 4 | const log = require('./log.js')('rabbot.acknack'); 5 | 6 | /* log 7 | * `rabbot.acknack` 8 | * `debug` 9 | * resolution operation on a series of tags 10 | * resolution operation on ALL tags 11 | * queue has no pending tags 12 | * new pending tag added 13 | * user performed message operation 14 | * `error` 15 | * message operation failed 16 | */ 17 | 18 | const calls = { 19 | ack: '_ack', 20 | nack: '_nack', 21 | reject: '_reject' 22 | }; 23 | 24 | const AckBatch = function (name, connectionName, resolver) { 25 | this.name = name; 26 | this.connectionName = connectionName; 27 | this.resolver = resolver; 28 | this.reset(); 29 | }; 30 | 31 | AckBatch.prototype._ack = function (tag, inclusive) { 32 | this.lastAck = tag; 33 | this._resolveTag(tag, 'ack', inclusive); 34 | }; 35 | 36 | AckBatch.prototype._ackOrNackSequence = function () { 37 | // try { 38 | const firstMessage = this.messages[0]; 39 | if (firstMessage === undefined) { 40 | return; 41 | } 42 | const firstStatus = firstMessage.status; 43 | let sequenceEnd = firstMessage.tag; 44 | const call = calls[firstStatus]; 45 | if (firstStatus === 'pending') { 46 | // empty 47 | } else { 48 | for (let i = 1; i < this.messages.length - 1; i++) { 49 | if (this.messages[i].status !== firstStatus) { 50 | break; 51 | } 52 | sequenceEnd = this.messages[i].tag; 53 | } 54 | if (call) { 55 | this[call](sequenceEnd, true); 56 | } 57 | } 58 | }; 59 | 60 | AckBatch.prototype._firstByStatus = function (status) { 61 | for (let i = 0; i < this.messages.length; i++) { 62 | if (this.messages[i].status === status) { 63 | return this.messages[i]; 64 | } 65 | } 66 | return undefined; 67 | }; 68 | 69 | AckBatch.prototype._findIndex = function (status) { 70 | for (let i = 0; i < this.messages.length; i++) { 71 | if (this.messages[i].status === status) { 72 | return i; 73 | } 74 | } 75 | return -1; 76 | }; 77 | 78 | AckBatch.prototype._lastByStatus = function (status) { 79 | for (let i = this.messages.length - 1; i >= 0; i--) { 80 | if (this.messages[i].status === status) { 81 | return this.messages[i]; 82 | } 83 | } 84 | return undefined; 85 | }; 86 | 87 | AckBatch.prototype._nack = function (tag, inclusive) { 88 | this.lastNack = tag; 89 | this._resolveTag(tag, 'nack', inclusive); 90 | }; 91 | 92 | AckBatch.prototype._reject = function (tag, inclusive) { 93 | this.lastReject = tag; 94 | this._resolveTag(tag, 'reject', inclusive); 95 | }; 96 | 97 | AckBatch.prototype._processBatch = function () { 98 | this.acking = this.acking !== undefined ? this.acking : false; 99 | if (!this.acking) { 100 | this.acking = true; 101 | const hasPending = (this._findIndex('pending') >= 0); 102 | const hasAck = this.firstAck !== undefined; 103 | const hasNack = this.firstNack !== undefined; 104 | const hasReject = this.firstReject !== undefined; 105 | if (!hasPending && !hasNack && hasAck && !hasReject) { 106 | // just acks 107 | this._resolveAll('ack', 'firstAck', 'lastAck'); 108 | } else if (!hasPending && hasNack && !hasAck && !hasReject) { 109 | // just nacks 110 | this._resolveAll('nack', 'firstNack', 'lastNack'); 111 | } else if (!hasPending && !hasNack && !hasAck && hasReject) { 112 | // just rejects 113 | this._resolveAll('reject', 'firstReject', 'lastReject'); 114 | } else if (hasNack || hasAck || hasReject) { 115 | // acks, nacks or rejects 116 | this._ackOrNackSequence(); 117 | this.acking = false; 118 | } else { 119 | // nothing to do 120 | this.resolver('waiting'); 121 | this.acking = false; 122 | } 123 | } 124 | }; 125 | 126 | AckBatch.prototype._resolveAll = function (status, first, last) { 127 | const count = this.messages.length; 128 | const emitEmpty = function () { 129 | // process.nextTick( function() { 130 | setTimeout(function () { 131 | this.emit('empty'); 132 | }.bind(this), 10); 133 | }.bind(this); 134 | if (this.messages.length > 0) { 135 | const lastTag = this._lastByStatus(status).tag; 136 | log.debug('%s ALL (%d) tags on %s up to %d - %s.', 137 | status, 138 | this.messages.length, 139 | this.name, 140 | lastTag, 141 | this.connectionName); 142 | this.resolver(status, { tag: lastTag, inclusive: true }) 143 | .then(function () { 144 | this[last] = lastTag; 145 | this._removeByStatus(status); 146 | this[first] = undefined; 147 | if (count > 0 && this.messages.length === 0) { 148 | log.debug('No pending tags remaining on queue %s - %s', this.name, this.connectionName); 149 | // The following setTimeout is the only thing between an insideous heisenbug and your sanity: 150 | // The promise for ack/nack will resolve on the channel before the server has processed it. 151 | // Without the setTimeout, if there is a pending cleanup/shutdown on the channel from the queueFsm, 152 | // the channel close will complete and cause the server to ignore the outstanding ack/nack command. 153 | // I lost HOURS on this because doing things that slow down the processing of the close cause 154 | // the bug to disappear. 155 | // Hackfully yours, 156 | // Alex 157 | emitEmpty(); 158 | } 159 | this.acking = false; 160 | }.bind(this)); 161 | } 162 | }; 163 | 164 | AckBatch.prototype._resolveTag = function (tag, operation, inclusive) { 165 | const removed = this._removeUpToTag(tag); 166 | const nextAck = this._firstByStatus('ack'); 167 | const nextNack = this._firstByStatus('nack'); 168 | const nextReject = this._firstByStatus('reject'); 169 | this.firstAck = nextAck ? nextAck.tag : undefined; 170 | this.firstNack = nextNack ? nextNack.tag : undefined; 171 | this.firstReject = nextReject ? nextReject.tag : undefined; 172 | log.debug('%s %d tags (%s) on %s - %s. (Next ack: %d, Next nack: %d, Next reject: %d)', 173 | operation, 174 | removed.length, 175 | inclusive ? 'inclusive' : 'individual', 176 | this.name, 177 | this.connectionName, 178 | this.firstAck || 0, 179 | this.firstNack || 0, 180 | this.firstReject || 0); 181 | this.resolver(operation, { tag: tag, inclusive: inclusive }); 182 | }; 183 | 184 | AckBatch.prototype._removeByStatus = function (status) { 185 | this.messages = this.messages.reduce((acc, message) => { 186 | if (message.status !== status) { 187 | acc.push(message); 188 | } 189 | return acc; 190 | }, []); 191 | }; 192 | 193 | AckBatch.prototype._removeUpToTag = function (tag) { 194 | let removed = 0; 195 | this.messages = this.messages.reduce((acc, message) => { 196 | if (message.tag > tag) { 197 | acc.push(message); 198 | } else { 199 | removed++; 200 | } 201 | return acc; 202 | }, []); 203 | return removed; 204 | }; 205 | 206 | AckBatch.prototype.addMessage = function (message) { 207 | this.receivedCount++; 208 | const status = message; 209 | this.messages.push(status); 210 | log.debug('New pending tag %d on queue %s - %s', status.tag, this.name, this.connectionName); 211 | }; 212 | 213 | AckBatch.prototype.changeName = function (name) { 214 | this.name = name; 215 | }; 216 | 217 | AckBatch.prototype.getMessageOps = function (tag) { 218 | return new TrackedMessage(tag, this); 219 | }; 220 | 221 | class TrackedMessage { 222 | constructor (tag, batch) { 223 | this.tag = tag; 224 | this.status = 'pending'; 225 | this.batch = batch; 226 | } 227 | 228 | ack () { 229 | this.status = 'ack'; 230 | this.batch.firstAck = this.batch.firstAck || this.tag; 231 | log.debug("Marking tag %d as %s'd on queue %s - %s", this.tag, this.status, this.batch.name, this.batch.connectionName); 232 | } 233 | 234 | nack () { 235 | this.status = 'nack'; 236 | this.batch.firstNack = this.batch.firstNack || this.tag; 237 | log.debug("Marking tag %d as %s'd on queue %s - %s", this.tag, this.status, this.batch.name, this.batch.connectionName); 238 | } 239 | 240 | reject () { 241 | this.status = 'reject'; 242 | this.batch.firstReject = this.batch.firstReject || this.tag; 243 | log.debug('Marking tag %d as %sed on queue %s - %s', this.tag, this.status, this.batch.name, this.batch.connectionName); 244 | } 245 | } 246 | 247 | AckBatch.prototype.ignoreSignal = function () { 248 | if (this.signalSubscription) { 249 | this.signalSubscription.unsubscribe(); 250 | } 251 | }; 252 | 253 | AckBatch.prototype.listenForSignal = function () { 254 | if (!this.signalSubscription) { 255 | this.signalSubscription = signal.subscribe('#', () => { 256 | this._processBatch(); 257 | }); 258 | } 259 | }; 260 | 261 | AckBatch.prototype.reset = function () { 262 | this.lastAck = -1; 263 | this.lastNack = -1; 264 | this.lastReject = -1; 265 | this.firstAck = undefined; 266 | this.firstNack = undefined; 267 | this.firstReject = undefined; 268 | this.messages = []; 269 | this.receivedCount = 0; 270 | }; 271 | 272 | Monologue.mixInto(AckBatch); 273 | 274 | module.exports = AckBatch; 275 | -------------------------------------------------------------------------------- /spec/behavior/exchangeFsm.spec.js: -------------------------------------------------------------------------------- 1 | require('../setup.js'); 2 | const exchangeFsm = require('../../src/exchangeFsm'); 3 | const emitter = require('./emitter'); 4 | const defer = require('../../src/defer'); 5 | const noop = () => {}; 6 | const _ = require('lodash'); 7 | 8 | function exchangeFn (options) { 9 | const channel = { 10 | name: options.name, 11 | type: options.type, 12 | channel: emitter(), 13 | define: noop, 14 | release: noop, 15 | publish: noop 16 | }; 17 | const channelMock = sinon.mock(channel); 18 | 19 | return { 20 | mock: channelMock, 21 | factory: function () { 22 | return Promise.resolve(channel); 23 | } 24 | }; 25 | } 26 | 27 | describe('Exchange FSM', function () { 28 | describe('when connection is unreachable', function () { 29 | let connection, topology, exchange, channelMock, options, error; 30 | let published; 31 | before(function (done) { 32 | options = { name: 'test', type: 'test' }; 33 | connection = emitter(); 34 | connection.addExchange = noop; 35 | topology = emitter(); 36 | 37 | const ex = exchangeFn(options); 38 | channelMock = ex.mock; 39 | channelMock 40 | .expects('define') 41 | .once() 42 | .returns({ then: noop }); 43 | 44 | exchange = exchangeFsm(options, connection, topology, {}, ex.factory); 45 | published = [1, 2, 3].map(() => exchange.publish({}).then(null, e => e.message)); 46 | exchange.once('failed', function (err) { 47 | error = err; 48 | done(); 49 | }).once(); 50 | connection.raise('unreachable'); 51 | }); 52 | 53 | it('should have emitted failed with an error', function () { 54 | return error.toString().should.equal('Error: Could not establish a connection to any known nodes.'); 55 | }); 56 | 57 | it('should reject all published promises', function () { 58 | return published.map((promise) => 59 | promise.should.eventually.equal('Could not establish a connection to any known nodes.') 60 | ); 61 | }); 62 | 63 | it('should be in unreachable state', function () { 64 | exchange.state.should.equal('unreachable'); 65 | }); 66 | 67 | describe('when publishing in unreachable state', function () { 68 | let error; 69 | 70 | before(function () { 71 | return exchange.publish({}).catch(function (err) { 72 | error = err; 73 | }); 74 | }); 75 | 76 | it('should reject publish with an error', function () { 77 | error.toString().should.equal('Error: Could not establish a connection to any known nodes.'); 78 | }); 79 | 80 | it('should clean up the "failed" subscription', function () { 81 | exchange._subscriptions.failed.should.have.lengthOf(0); 82 | }); 83 | }); 84 | 85 | describe('when checking in unreachable state', function () { 86 | it('should reject check with an error', function () { 87 | return exchange.check().should.be.rejectedWith('Could not establish a connection to any known nodes.'); 88 | }); 89 | }); 90 | }); 91 | 92 | describe('when definition has failed with error', function () { 93 | let connection, topology, exchange, channelMock, options; 94 | let published; 95 | before(function () { 96 | options = { name: 'test', type: 'test' }; 97 | connection = emitter(); 98 | connection.addExchange = noop; 99 | topology = emitter(); 100 | 101 | const ex = exchangeFn(options); 102 | channelMock = ex.mock; 103 | const deferred = defer(); 104 | channelMock 105 | .expects('define') 106 | .once() 107 | .returns(deferred.promise); 108 | 109 | exchange = exchangeFsm(options, connection, topology, {}, ex.factory); 110 | published = [1, 2, 3].map(() => 111 | exchange.publish({}) 112 | .then(null, (err) => err.message) 113 | ); 114 | deferred.reject(new Error('nope')); 115 | return Promise.all(published); 116 | }); 117 | 118 | it('should be in failed state', function () { 119 | exchange.state.should.equal('failed'); 120 | }); 121 | 122 | it('should reject all published promises', function () { 123 | published.forEach((promise) => { 124 | promise.should.eventually.equal('nope'); 125 | }); 126 | }); 127 | 128 | describe('when publishing in unreachable state', function () { 129 | let error; 130 | 131 | before(function () { 132 | return exchange.publish({}).catch(function (err) { 133 | error = err; 134 | }); 135 | }); 136 | 137 | it('should reject publish with an error', function () { 138 | error.toString().should.equal('Error: nope'); 139 | }); 140 | 141 | it('should clean up the "failed" subscription', function () { 142 | exchange._subscriptions.failed.should.have.lengthOf(0); 143 | }); 144 | }); 145 | 146 | describe('when checking in unreachable state', function () { 147 | it('should reject check with an error', function () { 148 | return exchange.check().should.be.rejectedWith('nope'); 149 | }); 150 | }); 151 | }); 152 | 153 | describe('when initializing succeeds', function () { 154 | let connection, topology, exchange, ex, channelMock, options, error; 155 | 156 | before(function (done) { 157 | options = { name: 'test', type: 'test' }; 158 | connection = emitter(); 159 | connection.addExchange = noop; 160 | topology = emitter(); 161 | 162 | ex = exchangeFn(options); 163 | channelMock = ex.mock; 164 | channelMock 165 | .expects('define') 166 | .once() 167 | .returns(Promise.resolve()); 168 | 169 | exchange = exchangeFsm(options, connection, topology, {}, ex.factory); 170 | exchange.on('failed', function (err) { 171 | error = err; 172 | done(); 173 | }).once(); 174 | exchange.on('defined', function () { 175 | done(); 176 | }).once(); 177 | }); 178 | 179 | it('should not have failed', function () { 180 | should.not.exist(error); 181 | }); 182 | 183 | it('should be in ready state', function () { 184 | exchange.state.should.equal('ready'); 185 | }); 186 | 187 | describe('when publishing in ready state', function () { 188 | let promise; 189 | 190 | before(function () { 191 | channelMock 192 | .expects('publish') 193 | .once() 194 | .returns(Promise.resolve(true)); 195 | 196 | promise = exchange.publish({}); 197 | 198 | return promise; 199 | }); 200 | 201 | it('should resolve publish without error', function () { 202 | return promise.should.be.fulfilled; 203 | }); 204 | 205 | it('should clean up the "failed" subscription', function () { 206 | // Should only have a single failed subscription from the outer "before" block 207 | exchange._subscriptions.failed.should.have.lengthOf(1); 208 | }); 209 | }); 210 | 211 | describe('when checking in ready state', function () { 212 | it('should resolve check without error', function () { 213 | exchange.check().should.be.fulfilled; // eslint-disable-line no-unused-expressions 214 | }); 215 | }); 216 | 217 | describe('when channel is closed', function () { 218 | before(function (done) { 219 | channelMock 220 | .expects('define') 221 | .once() 222 | .returns(Promise.resolve()); 223 | 224 | exchange.on('defined', function () { 225 | done(); 226 | }).once(); 227 | 228 | exchange.once('closed', function () { 229 | exchange.check(); 230 | }); 231 | 232 | ex.factory().then(function (e) { 233 | e.channel.raise('closed'); 234 | }); 235 | }); 236 | 237 | it('should reinitialize without error', function () { 238 | should.not.exist(error); 239 | }); 240 | }); 241 | 242 | describe('when releasing', function () { 243 | before(function () { 244 | exchange.published.add({}); 245 | exchange.published.add({}); 246 | exchange.published.add({}); 247 | 248 | channelMock 249 | .expects('release') 250 | .once() 251 | .resolves(); 252 | 253 | process.nextTick(function () { 254 | exchange.published.remove({ sequenceNo: 0 }); 255 | exchange.published.remove({ sequenceNo: 1 }); 256 | exchange.published.remove({ sequenceNo: 2 }); 257 | }); 258 | 259 | return exchange.release(); 260 | }); 261 | 262 | it('should remove handlers from topology and connection', function () { 263 | _.flatten(_.values(connection.handlers)).length.should.equal(1); 264 | _.flatten(_.values(topology.handlers)).length.should.equal(0); 265 | }); 266 | 267 | it('should release channel instance', function () { 268 | should.not.exist(exchange.channel); 269 | }); 270 | 271 | describe('when publishing to a released channel', function () { 272 | before(function () { 273 | channelMock 274 | .expects('define') 275 | .never(); 276 | 277 | channelMock 278 | .expects('publish') 279 | .never(); 280 | }); 281 | 282 | it('should reject publish', function () { 283 | return exchange.publish({}).should.be.rejectedWith('Cannot publish to exchange \'test\' after intentionally closing its connection'); 284 | }); 285 | 286 | it('should not make any calls to underlying exchange channel', function () { 287 | channelMock.verify(); 288 | }); 289 | }); 290 | }); 291 | 292 | after(function () { 293 | connection.reset(); 294 | topology.reset(); 295 | channelMock.restore(); 296 | }); 297 | }); 298 | }); 299 | -------------------------------------------------------------------------------- /docs/publishing.md: -------------------------------------------------------------------------------- 1 | # Publishing 2 | 3 | In confirm mode (the default for exchanges), 4 | the publish call returns a promise that is only resolved once the broker has confirmed the publish. 5 | (See [Publisher Acknowledgments](https://www.rabbitmq.com/confirms.html) for more details.) 6 | If a configured timeout is reached, or in the rare event that the broker rejects the message, the promise will be rejected. 7 | More commonly, the connection to the broker could be lost before the message is confirmed, and you end up with a message in "limbo". 8 | foo-foo-mq keeps a list of unconfirmed messages that have been published _in memory only_. 9 | Once a connection is available and the topology is in place, foo-foo-mq will send messages in the order of the publish calls. 10 | In the event of a disconnection or unreachable broker, all publish promises that have not been resolved are rejected. 11 | 12 | Publish timeouts can be set per message, per exchange or per connection. 13 | The most specific value overrides any set at a higher level. 14 | There are no default timeouts set at any level. 15 | The timer is started as soon as `publish` is called 16 | and only cancelled once foo-foo-mq is able to make the publish call on the actual exchange's channel. 17 | The timeout is cancelled once `publish` is called and will not result in a rejected promise due to time spent waiting on a confirmation. 18 | 19 | > Caution: foo-foo-mq does _not_ limit the growth of pending published messages. 20 | > If a service cannot connect to Rabbit due to misconfiguration or the broker being down, publishing lots of messages can lead to out-of-memory errors. 21 | > It is the consuming service's responsibility to handle these kinds of scenarios. 22 | 23 | Confirm mode is not without an overhead cost. 24 | This can be turned off, per exchange, by setting `noConfirm: true`. 25 | Confirmation results in increased memory overhead on the client and broker. 26 | When off, the promise will _always_ resolve when the connection and exchange are available. 27 | 28 | #### Serializers 29 | 30 | foo-foo-mq associates serialization techniques for messages with mimeTypes which can now be set when publishing a message. 31 | Out of the box, it really only supports 3 types of serialization: 32 | 33 | * `"text/plain"` 34 | * `"application/json"` 35 | * `"application/octet-stream"` 36 | 37 | You can register your own serializers using `addSerializer` but make sure to do so on both the sending and receiving side of the message. 38 | 39 | ## `rabbit.publish( exchangeName, options, [connectionName] )` 40 | 41 | Things to remember when publishing a message: 42 | 43 | * A type sepcifier is required so that the recipient knows what kind of message it is getting and which handler should process it. 44 | * If `contentType` is provided, then its value will be used for the message's contentType. 45 | * If `body` is an object or an array, it will be serialized as JSON and `contentType` will be "application/json". 46 | * If `body` is a string, it will be sent as a UTF-8 encoded string and `contentType` will be "text/plain". 47 | * If `body` is a buffer, it will be sent as a byte array and `contentType` will be "application/octet-stream". 48 | * By default, the type specifier will be used if no routing key is undefined. 49 | * Use a routing key of `""` to prevent the type specifier from being used as the routing key. 50 | * Non-persistent messages in a queue will be lost on server restart, default is non-persistent. 51 | Persistence can be set on either an exchange when it is created via addExchange, 52 | or when sending a message (required when using "default" exchanges since non-persistent publish is the default). 53 | 54 | This example shows all of the available properties (including those which get set by default): 55 | 56 | ### Example 57 | ```javascript 58 | rabbit.publish( "exchange.name", 59 | { 60 | routingKey: "hi", 61 | type: "company.project.messages.textMessage", 62 | correlationId: "one", 63 | contentType: "application/json", 64 | body: { text: "hello!" }, 65 | messageId: "100", 66 | expiresAfter: 1000, // TTL in ms, in this example 1 second 67 | timestamp: 1588479232215, // posix timestamp (long) 68 | mandatory: true, // Must be set to true for onReturned to receive unqueued message 69 | persistent: true, // If either message or exchange defines persistent=true queued messages will be saved to disk. 70 | headers: { 71 | random: "application specific value" 72 | }, 73 | timeout: 1000 // ms to wait before cancelling the publish and rejecting the promise 74 | }, 75 | connectionName // another optional way to provide a specific connection name if needed 76 | ); 77 | ``` 78 | 79 | ## `rabbit.request( exchangeName, options, [connectionName] )` 80 | 81 | This works just like a publish except that the promise returned provides the response (or responses) from the other side. 82 | A `replyTimeout` is available in the options 83 | and controls how long foo-foo-mq will wait for a reply before removing the subscription for the request to prevent memory leaks. 84 | 85 | > Note: the default replyTimeout will be double the publish timeout or 1 second if no publish timeout was ever specified. 86 | 87 | Request provides for two ways to get multiple responses; 88 | one is to allow a single replier to stream a set of responses back, 89 | and the other is to send a request to multiple potential responders and wait until a specific number comes back. 90 | 91 | ### Expecting A Singe Reply 92 | 93 | ```javascript 94 | // request side 95 | const parts = []; 96 | rabbit.request('request.ex', { 97 | type: 'request', 98 | body: id 99 | }) 100 | .then( reply => { 101 | // done - do something with all the data? 102 | reply.ack(); 103 | }); 104 | 105 | // receiver sides 106 | rabbit.handle('request', (req) => { 107 | req.reply(database.get(req.id)); 108 | }); 109 | ``` 110 | 111 | ### Expecting A Stream 112 | 113 | `reply` takes an additional hash argument where you can set `more` to `true` to indicate there are more messages incoming as part of the reply. 114 | 115 | In this case, the third argument to the `request` function will get every message **except** the last. 116 | 117 | ```javascript 118 | // request side 119 | const parts = []; 120 | rabbit.request('request.ex', { 121 | type: 'request', 122 | body: id 123 | }, 124 | reply => { 125 | parts.push(part); 126 | part.ack(); 127 | }) 128 | .then( final => { 129 | // done - do something with all the data? 130 | final.ack(); 131 | }); 132 | 133 | // receiver side 134 | rabbit.handle('request', (req) => { 135 | const stream = data.getById(req.body); 136 | stream.on('data', data => { 137 | req.reply(data, { more: true }); 138 | }); 139 | stream.on('end', () => { 140 | req.reply({ body: 'done' }); 141 | }); 142 | stream.on('error', (err) => { 143 | req.reply({ body: { error: true, detail: err.message }); 144 | }); 145 | }); 146 | ``` 147 | 148 | ### Scatter-Gather 149 | 150 | In scatter-gather, the recipients don't know how many of them exist 151 | and don't have to be aware that they are participating in scatter-gather/race-conditions. 152 | 153 | They just reply. 154 | The limit is applied on the requesting side by setting an `expects` property on the outgoing message 155 | to let foo-foo-mq know how many messages to collect before stopping and considering the request satisfied. 156 | 157 | Normally, this is done with multiple responders on the other side of a topic or fanout exchange. 158 | 159 | > !IMPORTANT! - messages beyond the limit are treated as unhandled. 160 | > You will need to have an unhandled message strategy in place 161 | > or at least understand how foo-foo-mq deals with them by default. 162 | 163 | ```javascript 164 | // request side 165 | const parts = []; 166 | rabbit.request('request.ex', { 167 | type: 'request', 168 | body: id, 169 | limit: 3 // will stop after 3 even if many more reply 170 | }, 171 | reply => { 172 | parts.push(part); 173 | part.ack(); 174 | }) 175 | .then( final => { 176 | // done - do something with all the data? 177 | final.ack(); 178 | }); 179 | 180 | // receiver sides 181 | rabbit.handle('request', (req) => { 182 | req.reply(database.get(req.id)); 183 | }); 184 | ``` 185 | 186 | ## `rabbit.bulkPublish( set, [connectionName] )` 187 | 188 | This creates a promise for a set of publishes to one or more exchanges on the same connection. 189 | 190 | It is a little more efficient than calling `publish` repeatedly 191 | as it performs the precondition checks up-front a single time before it begins the publishing. 192 | 193 | It supports two separate formats for specifying a set of messages: hash and array. 194 | 195 | ### Hash Format 196 | 197 | Each key is the name of the exchange to which to publish, and the value is an array of messages to send. 198 | Each element in the array follows the same format as the `publish` options. 199 | 200 | The exchanges are processed serially, 201 | so this option will _not_ work if you want finer control over sending messages to multiple exchanges in interleaved order. 202 | 203 | ```javascript 204 | rabbit.publish({ 205 | 'exchange-1': [ 206 | { type: 'one', body: '1' }, 207 | { type: 'one', body: '2' } 208 | ], 209 | 'exchange-2': [ 210 | { type: 'two', body: '1' }, 211 | { type: 'two', body: '2' } 212 | ] 213 | }).then( 214 | () => // a list of the messages of that succeeded, 215 | failed => // a list of failed messages and the errors `{ err, message }` 216 | ) 217 | ``` 218 | 219 | ### Array Format 220 | 221 | Each element in the array follows the format of `publish`'s option 222 | but requires the `exchange` property to control which exchange to publish each message to. 223 | 224 | ```javascript 225 | rabbit.publish([ 226 | { type: 'one', body: '1', exchange: 'exchange-1' }, 227 | { type: 'one', body: '2', exchange: 'exchange-1' }, 228 | { type: 'two', body: '1', exchange: 'exchange-2' }, 229 | { type: 'two', body: '2', exchange: 'exchange-2' } 230 | ]).then( 231 | () => // a list of the messages of that succeeded, 232 | failed => // a list of failed messages and the errors `{ err, message }` 233 | ) 234 | ``` 235 | --------------------------------------------------------------------------------