├── .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 | [](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 |
--------------------------------------------------------------------------------