├── test ├── mocha.opts ├── amqp.js ├── native.js ├── stomp.js ├── lib-jobs.js ├── lib-topic.js ├── fixtures │ ├── harness.js │ └── manager.js ├── clusters.js └── generic.js ├── docs.json ├── .jshintrc ├── .gitignore ├── EXAMPLE.md ├── .sl-blip.js ├── Makefile ├── lib ├── dbg.js ├── adapters │ ├── native │ │ ├── index.js │ │ ├── pushqueue.js │ │ ├── publishqueue.js │ │ ├── workqueue.js │ │ ├── pullqueue.js │ │ ├── subscribequeue.js │ │ ├── connection.js │ │ └── broker.js │ ├── stomp.js │ └── amqp.js ├── jobs.js └── topic.js ├── package.json ├── index.js ├── CHANGES.md ├── README.md ├── CONTRIBUTING.md ├── api.md └── LICENSE.md /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --check-leaks 2 | -------------------------------------------------------------------------------- /docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "content": [ 3 | "api.md" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase": true 3 | , "quotmark": "single" 4 | , "eqnull": true 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | README.html 2 | clustermq-*.tgz 3 | coverage.html 4 | local.mk 5 | node_modules 6 | npm-debug.log 7 | slmq-*-test 8 | _* 9 | -------------------------------------------------------------------------------- /EXAMPLE.md: -------------------------------------------------------------------------------- 1 | For an example of use, see the cluster example in the 2 | [SL Node Examples](https://github.com/strongloop/slnode-examples). 3 | -------------------------------------------------------------------------------- /.sl-blip.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2015,2016. All Rights Reserved. 2 | // Node module: strong-tools 3 | // This file is licensed under the MIT License. 4 | // License text available at https://opensource.org/licenses/MIT 5 | // for debugging: -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # Makefile 2 | 3 | -include local.mk 4 | 5 | .PHONY: test lint default 6 | 7 | default: lint test 8 | 9 | test: 10 | @npm test 11 | 12 | lint: 13 | @npm run lint 14 | 15 | glint: 16 | @gjslint --nojsdoc *.js test/*.js lib 17 | 18 | jenkins-build: jenkins-install jenkins-test 19 | 20 | jenkins-install: 21 | npm install 22 | 23 | jenkins-test: 24 | ./node_modules/.bin/mocha -R xunit > xunit.xml 25 | -------------------------------------------------------------------------------- /lib/dbg.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // dbg: console.log if NODE_SLMQ_DEBUG is in env, otherwise silent 7 | 8 | if (process.env.NODE_SLMQ_DEBUG) { 9 | module.exports = console.log; 10 | } else { 11 | module.exports = function() {}; 12 | } 13 | -------------------------------------------------------------------------------- /test/amqp.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var assert = require('assert'); 7 | var async = require('async'); 8 | var dbg = require('../lib/dbg'); 9 | var slmq = require('../'); 10 | 11 | 12 | var AMQP = {provider: 'amqp'}; 13 | 14 | 15 | describe.skip('open with amqp', function() { 16 | it('should error on a connect failure', function(done) { 17 | var mq = slmq.create({provider: 'amqp', port: 1}); 18 | mq.NAME = 'FIRST'; 19 | mq.open(function() { 20 | assert(false); // unreachable on failure 21 | }).on('error', function(er) { 22 | dbg('on err', mq.NAME, er); 23 | assert(er); 24 | done(); 25 | }); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /lib/adapters/native/index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var assert = require('assert'); 7 | var cluster = require('cluster'); 8 | var VERSION = require('../../../package.json').version; 9 | 10 | if(cluster._strongMqNative) { 11 | assert( 12 | cluster._strongMqNative.VERSION === VERSION, 13 | 'Multiple versions of strong-mq are being initialized.\n' + 14 | 'This version ' + VERSION + ' is incompatible with already initialized\n' + 15 | 'version ' + cluster._strongMqNative.VERSION + '.\n' 16 | ); 17 | module.exports = cluster._strongMqNative; 18 | return; 19 | } 20 | module.exports = require('./connection'); 21 | module.exports.VERSION = VERSION; 22 | cluster._strongMqNative = module.exports; 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "strong-mq", 3 | "version": "1.0.2", 4 | "description": "clustering of applications on top of message queues", 5 | "license": "Artistic-2.0", 6 | "main": "index.js", 7 | "scripts": { 8 | "test": "mocha --reporter spec", 9 | "lint": "./node_modules/.bin/jshint *.js test lib", 10 | "preinstall": "node .sl-blip.js || exit 0" 11 | }, 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/strongloop/strong-mq.git" 15 | }, 16 | "author": { 17 | "name": "Sam Roberts", 18 | "email": "sam@strongloop.com" 19 | }, 20 | "dependencies": { 21 | "amqp": "~0.1.6", 22 | "stomp-client": "~0.4.0", 23 | "underscore": "~1.4.4", 24 | "async": "~0.2.8" 25 | }, 26 | "devDependencies": { 27 | "mocha": "~1.9.0", 28 | "jshint": "~2.0.1" 29 | }, 30 | "optionalDependencies": {}, 31 | "engines": { 32 | "node": "*" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /test/native.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var assert = require('assert'); 7 | var mq = require('../index'); 8 | 9 | function listenerCount(emitter, event) { 10 | return emitter.listeners(event).length; 11 | } 12 | 13 | describe('native', function() { 14 | describe('should remove event listeners', function() { 15 | it('after multiple subscribes', function(done) { 16 | var connection = mq.create().open(); 17 | var sub = connection.createSubQueue('some-sub'); 18 | var messageListenerCount = listenerCount(sub.queue, 'publish'); 19 | sub.subscribe('xxx').subscribe('yyy'); 20 | connection.close(function() { 21 | assert.equal(listenerCount(sub.queue, 'publish'), messageListenerCount); 22 | done(); 23 | }); 24 | }); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /lib/jobs.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // Jobs: job flow control 7 | var async = require('async'); 8 | 9 | exports.delayed = delayed; 10 | 11 | // d = jobs.delayed([drained]) 12 | // 13 | // call drained every time all jobs are done 14 | // 15 | // d.push(job) 16 | // 17 | // job is function(done), it should call done() when complete 18 | // 19 | // d.start() 20 | // 21 | // start processing queue 22 | function delayed(drained) { 23 | var self = async.queue(function(task, callback) { 24 | task(callback); 25 | }, 0); 26 | self.start = function() { 27 | self.concurrency = 1; 28 | // Make async notice that concurrency has changed. 29 | self.push(function primer(done) { 30 | done(); 31 | }); 32 | return self; 33 | }; 34 | self.drain = drained; 35 | return self; 36 | } 37 | 38 | -------------------------------------------------------------------------------- /test/stomp.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var assert = require('assert'); 7 | var _stomp = require('../lib/adapters/stomp'); 8 | 9 | describe('stomp', function() { 10 | describe('url parsing', function() { 11 | function expect(url, options) { 12 | it('should parse '+url, function() { 13 | var parsed = _stomp._parseUrl(url); 14 | Object.keys(options).forEach(function(key) { 15 | assert.equal(parsed[key], options[key]); 16 | }); 17 | }); 18 | } 19 | expect('stomp://', { 20 | }); 21 | expect('stomp://a.b.c', { 22 | host: 'a.b.c', 23 | }); 24 | expect('stomp://a.b.c:99', { 25 | host: 'a.b.c', 26 | port: 99, 27 | }); 28 | expect('stomp://:99', { 29 | port: 99, 30 | }); 31 | expect('stomp://user@a.b.c:99', { 32 | host: 'a.b.c', 33 | port: 99, 34 | login: 'user', 35 | }); 36 | expect('stomp://user:pass@a.b.c:99', { 37 | host: 'a.b.c', 38 | port: 99, 39 | login: 'user', 40 | password: 'pass', 41 | }); 42 | }); 43 | }); 44 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // attempt to lazy-load providers on reference 7 | 8 | var assert = require('assert'); 9 | var copy = require('underscore').clone; 10 | var parse = require('url').parse; 11 | 12 | function urlProvider(url) { 13 | var protocol = parse(url).protocol; 14 | return protocol.split(':')[0]; 15 | } 16 | 17 | function extractProvider(options) { 18 | var provider; 19 | var url; 20 | if (typeof options == 'string') { 21 | provider = urlProvider(options); 22 | url = options; 23 | options = null; 24 | } else { 25 | provider = options.provider; 26 | url = null; 27 | options = copy(options); 28 | delete options.provider; 29 | } 30 | 31 | assert(provider, 'options must specify the provider'); 32 | 33 | return [provider, url, options]; 34 | } 35 | 36 | exports.create = function(options) { 37 | var parsed = extractProvider(options || { 38 | provider: 'native' 39 | }); 40 | var provider = parsed[0]; 41 | var url = parsed[1]; 42 | options = parsed[2]; 43 | var Connection = require('./lib/adapters/' + provider); 44 | return new Connection(provider, url, options); 45 | }; 46 | -------------------------------------------------------------------------------- /test/lib-jobs.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var delayed = require('../lib/jobs').delayed; 7 | var assert = require('assert'); 8 | 9 | describe('job queue', function() { 10 | it('should wait to start', function(done) { 11 | var started = false; 12 | var jobs = delayed(function() { 13 | //console.log('drained'); 14 | assert(started); 15 | done(); 16 | }); 17 | 18 | process.nextTick(function() { 19 | jobs.push(function(callback) { 20 | started = true; 21 | //console.log('first job'); 22 | process.nextTick(callback); 23 | }); 24 | }); 25 | 26 | process.nextTick(function() { 27 | process.nextTick(function() { 28 | assert(!started, 'two ticks later, still not started'); 29 | //console.log('before start'); 30 | jobs.start(); 31 | //console.log('after start'); 32 | }); 33 | }); 34 | }); 35 | 36 | it('should do jobs immediately after start', function(done) { 37 | var started = false; 38 | var jobs = delayed(function() { 39 | assert(started); 40 | done(); 41 | }).start(); 42 | 43 | jobs.push(function(callback) { 44 | started = true; 45 | callback(); 46 | }); 47 | 48 | assert(!started, 'pushed jobs are done in next tick'); 49 | }); 50 | }); 51 | -------------------------------------------------------------------------------- /lib/adapters/native/pushqueue.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # NativePushQueue 8 | // 9 | var assert = require('assert'); 10 | var cluster = require('cluster'); 11 | 12 | // 13 | // ## NativePushQueue `NativePushQueue(obj)` 14 | // 15 | // Creates a new instance of NativePushQueue with the following options: 16 | // 17 | function NativePushQueue(obj) { 18 | if (!(this instanceof NativePushQueue)) { 19 | return new NativePushQueue(obj); 20 | } 21 | 22 | obj = obj || {}; 23 | 24 | this.name = obj.name || ''; 25 | this.type = 'push'; 26 | this.queue = obj.queue || null; 27 | } 28 | NativePushQueue.createQueue = NativePushQueue; 29 | 30 | // 31 | // ## publish `publish(msg)` 32 | // 33 | // TODO: Description. 34 | // 35 | NativePushQueue.prototype.publish = cluster.isMaster ? masterPublish : workerPublish; 36 | function masterPublish(msg) { 37 | var self = this; 38 | 39 | assert(self.queue, 'No underlying queue was created.'); 40 | 41 | self.queue.push({ 42 | name: self.name, 43 | type: 'push', 44 | msg: msg 45 | }); 46 | 47 | return self; 48 | } 49 | function workerPublish(msg) { 50 | var self = this; 51 | 52 | process.send({ 53 | name: self.name, 54 | type: 'pushreq', 55 | msg: msg 56 | }); 57 | 58 | return self; 59 | } 60 | 61 | 62 | NativePushQueue.prototype.close = function() { 63 | }; 64 | 65 | 66 | module.exports = NativePushQueue; 67 | -------------------------------------------------------------------------------- /lib/adapters/native/publishqueue.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # NativePublishQueue 8 | // 9 | // TODO: Description. 10 | // 11 | 12 | var assert = require('assert'); 13 | var cluster = require('cluster'); 14 | var check = require('../../topic').check; 15 | 16 | // 17 | // ## NativePublishQueue `NativePublishQueue(obj)` 18 | // 19 | // Creates a new instance of NativePublishQueue with the following options: 20 | // 21 | function NativePublishQueue(obj) { 22 | if (!(this instanceof NativePublishQueue)) { 23 | return new NativePublishQueue(obj); 24 | } 25 | 26 | obj = obj || {}; 27 | 28 | this.name = obj.name; 29 | this.type = 'pub'; 30 | this.queue = obj.queue || null; 31 | } 32 | NativePublishQueue.createQueue = NativePublishQueue; 33 | 34 | 35 | NativePublishQueue.prototype.publish = cluster.isMaster ? masterPublish : workerPublish; 36 | function masterPublish(msg, topic) { 37 | var self = this; 38 | 39 | topic = check(topic); 40 | 41 | self.queue.publish(msg, topic); 42 | 43 | return self; 44 | } 45 | function workerPublish(msg, topic) { 46 | var self = this; 47 | 48 | topic = check(topic); 49 | 50 | process.send({ 51 | name: self.name, 52 | type: 'publishreq', 53 | topic: String(topic), 54 | msg: msg 55 | }); 56 | 57 | return self; 58 | } 59 | 60 | NativePublishQueue.prototype.close = function() { 61 | }; 62 | 63 | 64 | module.exports = NativePublishQueue; 65 | -------------------------------------------------------------------------------- /lib/topic.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // topic: utility code for topics 7 | 8 | var assert = require('assert'); 9 | var format = require('util').format; 10 | 11 | exports.valid = valid; 12 | exports.check = check; 13 | exports._escape = escape; 14 | exports.matcher = matcher; 15 | 16 | function valid(topic) { 17 | if (!isString(topic)) { 18 | return false; 19 | } 20 | 21 | if (topic === '') { 22 | return true; 23 | } 24 | 25 | if (/^\w+(?:\.\w+)*$/.test(topic)) { 26 | return true; 27 | } 28 | 29 | return false; 30 | } 31 | 32 | function check(pattern) { 33 | if (pattern == null) { 34 | pattern = ''; 35 | } 36 | 37 | if (!valid(pattern)) { 38 | // pattern can be a non-string! 39 | assert(false, format( 40 | 'Invalid topic %j. ' + 41 | 'Topic patterns must be .-separated alphanumeric words', 42 | pattern 43 | )); 44 | } 45 | 46 | return pattern; 47 | } 48 | 49 | function isString(obj) { 50 | return typeof obj === 'string' || obj instanceof String; 51 | } 52 | 53 | function escape(topic) { 54 | topic = check(topic); 55 | 56 | return topic.replace(/\./g, '\\.'); 57 | } 58 | 59 | function matcher(topic) { 60 | topic = escape(topic); 61 | 62 | // /^\b/ won't match 'fu', so bypass regex in this case 63 | if (topic === '') { 64 | return { 65 | test: function () { return true; } 66 | }; 67 | } 68 | 69 | return RegExp('^' + topic + '\\b'); 70 | } 71 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2016-11-03, Version 1.0.2 2 | ========================= 3 | 4 | * Note that strong-mq should not be used (Sam Roberts) 5 | 6 | * Update URLs in CONTRIBUTING.md (#31) (Ryan Graham) 7 | 8 | 9 | 2016-05-03, Version 1.0.1 10 | ========================= 11 | 12 | * update copyrights and license (Ryan Graham) 13 | 14 | * Refer to licenses with a link (Sam Roberts) 15 | 16 | * Use strongloop conventions for licensing (Sam Roberts) 17 | 18 | 19 | 2015-03-23, Version 1.0.0 20 | ========================= 21 | 22 | * package: correct license link (Sam Roberts) 23 | 24 | * Fix bad CLA URL in CONTRIBUTING.md (Ryan Graham) 25 | 26 | 27 | 2014-12-24, Version 0.0.9 28 | ========================= 29 | 30 | * package: remove unused marked dependency (Sam Roberts) 31 | 32 | 33 | 2014-10-21, Version 0.0.8 34 | ========================= 35 | 36 | * Update contribution guidelines (Ryan Graham) 37 | 38 | * Fixed link to npm node-amqp package (Ivan Fraixedes) 39 | 40 | * Update package license to match LICENSE.md (Sam Roberts) 41 | 42 | * Update link to doc (Rand McKinney) 43 | 44 | * doc: add CONTRIBUTING.md and LICENSE.md (Ben Noordhuis) 45 | 46 | * Corrected per DFD-67 (Rand McKinney) 47 | 48 | * Apply Dual MIT/StrongLoop license (Sam Roberts) 49 | 50 | * Reorganize documentation in README and api (Sam Roberts) 51 | 52 | * Update docs.json (Rand McKinney) 53 | 54 | * Update README.md (Rand McKinney) 55 | 56 | * Create api.md (Rand McKinney) 57 | 58 | * remove unused blanket dependencies (Sam Roberts) 59 | 60 | 61 | 2013-10-29, Version 0.0.7 62 | ========================= 63 | 64 | * Detect and fail on invalid multiple instantiation (Sam Roberts) 65 | 66 | * Multiple subscribes need multiple listeners removed (Sam Roberts) 67 | 68 | * Minor formatting per style guide. Rm'd dangling 'MQ' (Edmond Meinfelder) 69 | 70 | 71 | 2013-09-09, Version 0.0.6 72 | ========================= 73 | 74 | * lib/topic.js: better assert message (Miroslav Bajtos) 75 | 76 | * Adding docs.json as part of docs effort. (Edmond Meinfelder) 77 | 78 | * Rename github repository from sl-mq to strong-mq (Sam Roberts) 79 | 80 | * update coverage file name (slnode) 81 | 82 | 83 | 2013-07-16, Version 0.0.5 84 | ========================= 85 | 86 | * First release! 87 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # strong-mq: Clustering of Applications on Top of Message Queues 2 | 3 | ***DO NOT USE THIS:*** This code is an attempt to build a common API over 4 | multiple message queues so as to allow deploy time selection of the MQ to use. 5 | In practice, no one does this, they always know the MQ they are going to use. 6 | Also, this module by necessity exposes a subset of the underlying MQ libraries 7 | capabilities, so it is perpetually not working as desired. 8 | 9 | Use one of these directly: 10 | 11 | - https://www.npmjs.com/package/stomp-client 12 | - https://www.npmjs.com/package/amqp 13 | 14 | ## Overview 15 | 16 | [strong-mq](https://github.com/strongloop/strong-mq) is an abstraction layer 17 | over common message distribution patterns, and several different message queue 18 | implementations, including cluster-native messaging. 19 | 20 | It allows applications to be written against a single message queue style API, 21 | and then deployed either singly, or as a cluster, with deploy-time configuration 22 | of the messaging provider. Providers include native node clustering, allowing 23 | no-dependency deployment during test and development. Support for other 24 | providers is on-going, and 3rd parties will be able to add pluggable support for 25 | new message queue platforms. 26 | 27 | ## Installation 28 | 29 | % npm install strong-mq 30 | % npm test 31 | 32 | ### Multiple Versions of strong-mq Being Initialized 33 | 34 | If you get an assert during require of strong-mq about multiple versions being 35 | initialized, then some of the modules you are depending on use strong-mq, but do 36 | not specify it as a peerDependency. See 37 | [strongloop/strong-cluster-connect-store](https://github.com/strongloop/strong-cluster-connect-store/commit/dd00ed6978a676725c863e4ce0473bc8d2997d2f) 38 | as an example of how to correctly specify a dependency on strong-mq in a module. 39 | An application can depend on strong-mq with a normal dependency. 40 | 41 | ## Synopsis 42 | 43 | An example of connecting to a server and listening on a work queue: 44 | 45 | ```javascript 46 | var connection = require('strong-mq') 47 | .create('amqp://localhost') 48 | .open(); 49 | 50 | var push = connection.createPushQueue('todo-items'); 51 | push.publish({job: 'clean pool'}); 52 | 53 | var pull = connection.createPullQueue('todo-items'); 54 | pull.subscribe(function(msg) { 55 | console.log('TODO:', msg); 56 | connection.close(); 57 | }); 58 | ``` 59 | 60 | ## Documentation 61 | 62 | - See [API](api.md) and [StrongLoop](http://docs.strongloop.com/display/NODE/Strong+MQ) 63 | -------------------------------------------------------------------------------- /lib/adapters/native/workqueue.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # WorkQueue 8 | // 9 | // TODO: Description. 10 | // 11 | 12 | var dbg = require('../../dbg'); 13 | 14 | // 15 | // ## WorkQueue `WorkQueue(obj)` 16 | // 17 | // Creates a new instance of WorkQueue with the following options: 18 | // 19 | function WorkQueue(obj) { 20 | if (!(this instanceof WorkQueue)) { 21 | return new WorkQueue(obj); 22 | } 23 | 24 | obj = obj || {}; 25 | 26 | this.work = []; 27 | this.workers = []; 28 | this.nextWorker = 0; 29 | } 30 | WorkQueue.createQueue = WorkQueue; 31 | 32 | // 33 | // ## push `push(task)` 34 | // 35 | // TODO: Description. 36 | // 37 | WorkQueue.prototype.push = push; 38 | function push(task) { 39 | var self = this; 40 | 41 | if (self.workers.length) { 42 | // Round-robin amongst workers agnostic of worker Array size, which can 43 | // change on the fly as workers come and go. 44 | self.nextWorker = self.nextWorker % self.workers.length; 45 | self.workers[self.nextWorker].send(task); 46 | self.nextWorker++; 47 | } else { 48 | self.work.push(task); 49 | } 50 | 51 | return self; 52 | } 53 | 54 | // 55 | // ## flush `flush()` 56 | // 57 | // TODO: Description. 58 | // 59 | WorkQueue.prototype.flush = flush; 60 | function flush() { 61 | var self = this; 62 | var work = self.work; 63 | 64 | // Avoid re-pushing if we know there are no workers to receive 65 | if (!self.workers.length) { 66 | return self; 67 | } 68 | 69 | self.work = []; 70 | 71 | work.forEach(function(task) { 72 | self.push(task); 73 | }); 74 | 75 | return self; 76 | } 77 | 78 | // 79 | // ## pushWorker `pushWorker(worker)` 80 | // 81 | // TODO: Description. 82 | // 83 | WorkQueue.prototype.pushWorker = pushWorker; 84 | function pushWorker(worker) { 85 | var self = this; 86 | 87 | self.workers.push(worker); 88 | 89 | // Allow subscribe to return before pushing any-preexisting tasks 90 | process.nextTick(function() { 91 | self.flush(); 92 | }); 93 | 94 | return self; 95 | } 96 | 97 | // 98 | // ## popWorker `popWorker(worker)` 99 | // 100 | // TODO: Description. 101 | // 102 | WorkQueue.prototype.popWorker = popWorker; 103 | function popWorker(worker) { 104 | var self = this; 105 | var index = self.workers.indexOf(worker); 106 | 107 | if (index !== -1) { 108 | self.workers.splice(index, 1); 109 | } 110 | 111 | return self; 112 | } 113 | 114 | module.exports = WorkQueue; 115 | -------------------------------------------------------------------------------- /test/lib-topic.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var assert = require('assert'); 7 | 8 | var topic = require('../lib/topic'); 9 | 10 | describe('topic', function() { 11 | it('should be a string', function() { 12 | assert(topic.valid('')); 13 | assert(!topic.valid({})); 14 | assert(!topic.valid(1)); 15 | assert(!topic.valid(true)); 16 | }); 17 | 18 | it('should accept null and undefined', function() { 19 | assert.equal('', topic.check()); 20 | assert.equal('', topic.check(null)); 21 | assert.equal('', topic.check(undefined)); 22 | assert.equal('X', topic.check('X')); 23 | }); 24 | 25 | it('should be dot-seperated words', function() { 26 | assert(topic.valid('0')); 27 | assert(topic.valid('A')); 28 | assert(topic.valid('_')); 29 | assert(topic.valid('Ab0_z')); 30 | assert(topic.valid('Ab0_z._')); 31 | assert(topic.valid('Ab0_z._.a.9.Q')); 32 | 33 | assert(!topic.valid('.')); 34 | assert(!topic.valid('+')); 35 | assert(!topic.valid('a.')); 36 | assert(!topic.valid('Ab0_z._.')); 37 | }); 38 | 39 | it('should escape dots for use in regular expressions', function() { 40 | assert.equal(topic._escape('a.b.c'), 'a\\.b\\.c'); 41 | }); 42 | 43 | it('should match identical prefixes', function() { 44 | assert(topic.matcher('a').test('a')); 45 | assert(topic.matcher('a').test('a.c')); 46 | assert(topic.matcher('a').test('a.c.d')); 47 | 48 | assert(!topic.matcher('a').test('ab')); 49 | 50 | 51 | assert(topic.matcher('a0_._9_').test('a0_._9_')); 52 | 53 | assert(!topic.matcher('a0_z._9_').test('a0_._9_')); 54 | assert(!topic.matcher('a0_._9_').test('a0_z._9_')); 55 | assert(!topic.matcher('a0_._9_').test('a0_._9_z')); 56 | 57 | 58 | assert(topic.matcher('some').test('some.thing.really.deep')); 59 | assert(topic.matcher('some').test('some.thing.really')); 60 | assert(topic.matcher('some').test('some.thing')); 61 | assert(topic.matcher('some').test('some')); 62 | 63 | assert(!topic.matcher('some').test('something')); 64 | assert(!topic.matcher('some').test('som')); 65 | assert(!topic.matcher('some').test('')); 66 | 67 | 68 | assert(topic.matcher('').test('some.thing.really.deep')); 69 | assert(topic.matcher('').test('some.thing.really')); 70 | assert(topic.matcher('').test('some.thing')); 71 | assert(topic.matcher('').test('some')); 72 | assert(topic.matcher('').test('')); 73 | 74 | 75 | assert(topic.matcher('some.thing').test('some.thing.really.deep')); 76 | assert(topic.matcher('some.thing').test('some.thing.really')); 77 | assert(topic.matcher('some.thing').test('some.thing')); 78 | 79 | assert(!topic.matcher('some.thing').test('some')); 80 | assert(!topic.matcher('some.thing').test('')); 81 | }); 82 | 83 | }); 84 | -------------------------------------------------------------------------------- /lib/adapters/native/pullqueue.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # NativePullQueue 8 | // 9 | var assert = require('assert'); 10 | var cluster = require('cluster'); 11 | var EventEmitter = require('events').EventEmitter; 12 | var util = require('util'); 13 | 14 | // 15 | // ## NativePullQueue `NativePullQueue(obj)` 16 | // 17 | // Creates a new instance of NativePullQueue with the following options: 18 | // 19 | function NativePullQueue(obj) { 20 | if (!(this instanceof NativePullQueue)) { 21 | return new NativePullQueue(obj); 22 | } 23 | 24 | EventEmitter.call(this); 25 | 26 | obj = obj || {}; 27 | 28 | this.name = obj.name || ''; 29 | this.type = 'pull'; 30 | this.queue = obj.queue || null; 31 | } 32 | util.inherits(NativePullQueue, EventEmitter); 33 | NativePullQueue.createQueue = NativePullQueue; 34 | 35 | // 36 | // ## subscribe `subscribe([handler])` 37 | // 38 | // If provided, **handler** will be added as a `'message'` event listener for 39 | // this queue. 40 | // 41 | // XXX(sam) subscribe should be mandatory 42 | NativePullQueue.prototype.subscribe = subscribe; 43 | function subscribe(handler) { 44 | var self = this; 45 | 46 | this._init(); 47 | 48 | if (handler) { 49 | self.on('message', handler); 50 | } 51 | 52 | return self; 53 | } 54 | 55 | NativePullQueue.prototype.close = cluster.isMaster ? masterClose : workerClose; 56 | function masterClose() { 57 | var self = this; 58 | self.queue.popWorker(self); 59 | return self; 60 | } 61 | function workerClose() { 62 | var self = this; 63 | if (self._receive) { 64 | process.send({ 65 | type: 'stoppull', 66 | name: self.name 67 | }); 68 | process.removeListener('message', self._receive); 69 | } 70 | return self; 71 | } 72 | 73 | 74 | // Establishes internal state, event handlers, etc. 75 | // 76 | // XXX(sam) if a worker has two pull queues, with same name, then BOTH will get 77 | // the msg 78 | NativePullQueue.prototype._init = cluster.isMaster ? masterInit : workerInit; 79 | function masterInit() { 80 | var self = this; 81 | 82 | assert(self.queue, 'No underlying queue was created'); 83 | 84 | if (self.send) { 85 | return; // only do this once 86 | } 87 | 88 | self.send = function(data) { 89 | self.emit('message', data.msg); 90 | }; 91 | 92 | self.queue.pushWorker(self); 93 | 94 | return self; 95 | } 96 | function workerInit() { 97 | var self = this; 98 | 99 | if (self.receive) { 100 | return; // only do this once 101 | } 102 | 103 | self._receive = function(data) { 104 | if (data.name === self.name && data.type === 'push') { 105 | self.emit('message', data.msg); 106 | } 107 | }; 108 | 109 | process.on('message', self._receive); 110 | 111 | process.send({ 112 | type: 'startpull', 113 | name: self.name 114 | }); 115 | 116 | return self; 117 | } 118 | 119 | module.exports = NativePullQueue; 120 | -------------------------------------------------------------------------------- /lib/adapters/native/subscribequeue.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # NativeSubscribeQueue 8 | // 9 | var assert = require('assert'); 10 | var EventEmitter = require('events').EventEmitter; 11 | var cluster = require('cluster'); 12 | var util = require('util'); 13 | var matcher = require('../../topic').matcher; 14 | 15 | // 16 | // ## NativeSubscribeQueue `NativeSubscribeQueue(obj)` 17 | // 18 | // Creates a new instance of NativeSubscribeQueue with the following options: 19 | // 20 | function NativeSubscribeQueue(obj) { 21 | if (!(this instanceof NativeSubscribeQueue)) { 22 | return new NativeSubscribeQueue(obj); 23 | } 24 | 25 | EventEmitter.call(this); 26 | 27 | obj = obj || {}; 28 | 29 | this.name = obj.name; 30 | this.type = 'sub'; 31 | this.queue = obj.queue || null; 32 | this._onPublish = []; 33 | this._onMessage = []; 34 | } 35 | util.inherits(NativeSubscribeQueue, EventEmitter); 36 | NativeSubscribeQueue.createQueue = NativeSubscribeQueue; 37 | 38 | // 39 | // ## subscribe `subscribe(pattern, [handler])` 40 | // 41 | // Subscribes to **pattern**, increasing the types of `message` events emitted. 42 | // If provided, **handler** will be added as a `'message'` event listener for 43 | // this queue. 44 | // 45 | NativeSubscribeQueue.prototype.subscribe = cluster.isMaster ? masterSubscribe : workerSubscribe; 46 | function masterSubscribe(pattern, handler) { 47 | var self = this; 48 | var regexp = matcher(pattern); 49 | 50 | assert(self.queue, 'No underlying queue was created'); 51 | 52 | function onPublish(data) { 53 | if (regexp.test(data.topic)) { 54 | self.emit('message', data.msg); 55 | } 56 | }; 57 | 58 | self._onPublish.push(onPublish); 59 | self.queue.on('publish', onPublish); 60 | 61 | if (handler) { 62 | self.on('message', handler); 63 | } 64 | 65 | return self; 66 | } 67 | function workerSubscribe(pattern, handler) { 68 | var self = this; 69 | var regexp = matcher(pattern); 70 | 71 | function onMessage(data) { 72 | if (data.name === self.name && data.type === 'publish' && 73 | regexp.test(data.topic)) { 74 | self.emit('message', data.msg); 75 | } 76 | }; 77 | 78 | self._onMessage.push(onMessage); 79 | process.on('message', onMessage); 80 | 81 | if (handler) { 82 | self.on('message', handler); 83 | } 84 | 85 | return self; 86 | } 87 | 88 | function removeListeners(emitter, event, listeners) { 89 | callbacks.forEach(function(callback) { 90 | emitter.removeListener(event, callback); 91 | }); 92 | } 93 | 94 | NativeSubscribeQueue.prototype.close = cluster.isMaster ? masterClose : workerClose; 95 | function masterClose() { 96 | var self = this; 97 | self._onPublish.forEach(self.queue.removeListener.bind(self.queue, 'publish')); 98 | self._onPublish = []; 99 | return self; 100 | } 101 | function workerClose() { 102 | var self = this; 103 | self._onMessage.forEach(process.removeListener.bind(process, 'message')); 104 | self._onMessage = []; 105 | return self; 106 | } 107 | 108 | 109 | module.exports = NativeSubscribeQueue; 110 | -------------------------------------------------------------------------------- /lib/adapters/native/connection.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # NativeConnection 8 | // 9 | var cluster = require('cluster'); 10 | var dbg = require('../../dbg'); 11 | var broker = require('./broker'); 12 | var EventEmitter = require('events').EventEmitter; 13 | var util = require('util'); 14 | var NativePushQueue = require('./pushqueue'); 15 | var NativePullQueue = require('./pullqueue'); 16 | var NativePublishQueue = require('./publishqueue'); 17 | var NativeSubscribeQueue = require('./subscribequeue'); 18 | 19 | // Creates a new instance of NativeConnection. No options are exposed. 20 | function NativeConnection(provider, url, options) { 21 | EventEmitter.call(this); 22 | 23 | this.provider = provider; 24 | this._queues = []; 25 | } 26 | util.inherits(NativeConnection, EventEmitter); 27 | NativeConnection.createConnection = NativeConnection; 28 | 29 | // XXX(sam) optional, but it shouldn't be 30 | NativeConnection.prototype.open = function() { 31 | var self = this; 32 | return self; 33 | }; 34 | 35 | // XXX(sam) optional, but it shouldn't be 36 | NativeConnection.prototype.close = function (callback) { 37 | var self = this; 38 | 39 | self._queues.forEach(function (queue) { 40 | queue.close(); 41 | }); 42 | self._queues = []; 43 | 44 | if (callback) { 45 | process.nextTick(callback); 46 | } 47 | 48 | return self; 49 | }; 50 | 51 | // XXX(sam) refactor below to reduce cut-n-paste 52 | NativeConnection.prototype.createPushQueue = cluster.isMaster ? masterCreatePushQueue : workerCreatePushQueue; 53 | function masterCreatePushQueue(name) { 54 | var queue = NativePushQueue.createQueue({ 55 | name: name, 56 | queue: broker.getWorkQueue(name) 57 | }); 58 | this._queues.push(queue); 59 | return queue; 60 | } 61 | function workerCreatePushQueue(name) { 62 | return NativePushQueue.createQueue({ 63 | name: name 64 | }); 65 | } 66 | 67 | NativeConnection.prototype.createPullQueue = cluster.isMaster ? masterCreatePullQueue : workerCreatePullQueue; 68 | function masterCreatePullQueue(name) { 69 | var queue = NativePullQueue.createQueue({ 70 | name: name, 71 | queue: broker.getWorkQueue(name) 72 | }); 73 | this._queues.push(queue); 74 | return queue; 75 | } 76 | function workerCreatePullQueue(name) { 77 | return NativePullQueue.createQueue({ 78 | name: name 79 | }); 80 | } 81 | 82 | NativeConnection.prototype.createPubQueue = cluster.isMaster ? masterCreatePublishQueue : workerCreatePublishQueue; 83 | function masterCreatePublishQueue(name) { 84 | var queue = NativePublishQueue.createQueue({ 85 | name: name, 86 | queue: broker.getTopicQueue(name) 87 | }); 88 | this._queues.push(queue); 89 | return queue; 90 | } 91 | function workerCreatePublishQueue(name) { 92 | var self = this; 93 | 94 | return NativePublishQueue.createQueue({ 95 | name: name 96 | }); 97 | } 98 | 99 | NativeConnection.prototype.createSubQueue = cluster.isMaster ? masterCreateSubscribeQueue : workerCreateSubscribeQueue; 100 | function masterCreateSubscribeQueue(name) { 101 | var queue = NativeSubscribeQueue.createQueue({ 102 | name: name, 103 | queue: broker.getTopicQueue(name) 104 | }); 105 | this._queues.push(queue); 106 | return queue; 107 | } 108 | function workerCreateSubscribeQueue(name) { 109 | var self = this; 110 | 111 | return NativeSubscribeQueue.createQueue({ 112 | name: name 113 | }); 114 | } 115 | 116 | module.exports = NativeConnection; 117 | -------------------------------------------------------------------------------- /test/fixtures/harness.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // This "harness" is responsible for running a small cluster of processes that 7 | // send events to one another, finally recording the results to the file 8 | // system. While it runs the tests, it does not assert anything about the 9 | // results, that is left to the caller. 10 | // 11 | var cluster = require('cluster'); 12 | var manager = require('./manager').createManager(process.env); 13 | var WORKERS = 2; 14 | 15 | /* 16 | There are 17 tests per process with four messages sent per test. With 2 17 | workers, the total number of messages that should be recorded will be 84 18 | push/pull and 156 pub/sub, for 240 total. Each process: 19 | 20 | 1. Sends messages to itself over push/pull where the PushQueue was created first 21 | 2. Sends messages to itself over push/pull where the PullQueue was created first 22 | 3. Sends messages to master over push/pull 23 | 4. Sends messages to worker0 over push/pull 24 | 5. Sends messages to worker1 over push/pull 25 | 6. Sends messages to both workers over push/pull 26 | 7. Sends messages to all processes over push/pull 27 | 8. Sends messages to master over pub/sub with a generic topic 28 | 9. Sends messages to worker0 over pub/sub with a generic topic 29 | 10. Sends messages to worker1 over pub/sub with a generic topic 30 | 11. Sends messages to both workers over pub/sub with a generic topic 31 | 12. Sends messages to all processes over pub/sub with a generic topic 32 | 13. Sends messages to both workers over pub/sub with a worker0-specific topic 33 | 14. Sends messages to both workers over pub/sub with a worker1-specific topic 34 | 15. Sends messages to all processes over pub/sub with a master-specific topic 35 | 16. Sends messages to all processes over pub/sub with a worker0-specific topic 36 | 17. Sends messages to all processes over pub/sub with a worker1-specific topic 37 | */ 38 | manager 39 | .init(WORKERS) 40 | .runTestPush(process.env.id + '.pushfirst') 41 | .runTestPull(process.env.id + '.pushfirst') 42 | .runTestPull(process.env.id + '.pullfirst') 43 | .runTestPush(process.env.id + '.pullfirst') 44 | .runTestPush('master.work') 45 | .runTestPush('worker0.work') 46 | .runTestPush('worker1.work') 47 | .runTestPull(process.env.id + '.work') 48 | .runTestPull('all.work'); 49 | 50 | manager 51 | .runTestSubscribe(process.env.id + '.topic', 'test') 52 | .runTestSubscribe('all.topic', 'test') 53 | .runTestSubscribe('all.topic', process.env.id); 54 | 55 | // Note: Race conditions ahead. in the absence of explicit synchronization about 56 | // test start-stop, we use timeouts. Publishers need to wait until subscribers 57 | // are ready, and subscribers need to wait until all messages have been 58 | // received. This is a bit finicky, because timing depends on the system. 59 | setTimeout(function () { 60 | manager 61 | .runTestPush('workers.work') 62 | .runTestPush('all.work') 63 | .runTestPublish('master.topic', 'test') 64 | .runTestPublish('worker0.topic', 'test') 65 | .runTestPublish('worker1.topic', 'test') 66 | .runTestPublish('workers.topic', 'test') 67 | .runTestPublish('all.topic', 'test') 68 | .runTestPublish('all.topic', 'master') 69 | .runTestPublish('all.topic', 'worker0') 70 | .runTestPublish('all.topic', 'worker1') 71 | .runTestPublish('workers.topic', 'worker0') 72 | .runTestPublish('workers.topic', 'worker1'); 73 | }, 2000); 74 | 75 | // 76 | // Not only do only workers subscribe to the workers.work queue, but workers 77 | // should be shut down after a reasonable amount of time has been allotted for 78 | // the tests themselves. 79 | // 80 | if (cluster.isWorker) { 81 | manager 82 | .runTestPull('workers.work') 83 | .runTestSubscribe('workers.topic', 'test') 84 | .runTestSubscribe('workers.topic', process.env.id); 85 | 86 | setTimeout(process.exit, 5000); 87 | } else { 88 | // Close the connection, allowing harness to exit after its workers 89 | setTimeout(function() { 90 | manager.connection.close(); 91 | }, 6000); 92 | } 93 | -------------------------------------------------------------------------------- /lib/adapters/native/broker.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // cluster master broker 7 | // 8 | // This 'broker' lives in a cluster master, attaches to its fork and disconnect 9 | // events, and keeps track of the queues, and the connections to them. Those 10 | // connections and users of the queues can either be child workers, or 11 | // connections created in the master (a situation more typical during unit 12 | // testing). 13 | 14 | var assert = require('assert'); 15 | var cluster = require('cluster'); 16 | var EventEmitter = require('events').EventEmitter; 17 | var WorkQueue = require('./workqueue'); 18 | var VERSION = require('../../../package.json').version; 19 | 20 | // Only initialize a broker in the master 21 | if (cluster.isMaster) { 22 | assert(!cluster._strongMqNative, 'Multiple instantiation detected!'); 23 | 24 | exports._init = _init; 25 | exports._final = _final; 26 | exports._init(); 27 | } 28 | 29 | 30 | function _init() { 31 | var self = this; 32 | 33 | self._topicQueues = {}; 34 | self._workQueues = {}; 35 | 36 | // For each worker that is created, listen to its messages. 37 | self._onFork = function onFork(worker) { 38 | worker.on('message', function(data) { 39 | switch (data.type) { 40 | case 'publishreq': 41 | self.getTopicQueue(data.name).publish(data.msg, data.topic); 42 | break; 43 | case 'startpull': 44 | self.getWorkQueue(data.name).pushWorker(worker); 45 | break; 46 | case 'stoppull': 47 | self.getWorkQueue(data.name).popWorker(worker); 48 | break; 49 | case 'pushreq': 50 | data.type = 'push'; 51 | self.getWorkQueue(data.name).push(data); 52 | break; 53 | } 54 | }); 55 | }; 56 | 57 | // Scrub all work queues and remove the no-longer-extant worker. 58 | self._onDisconnect = function onDisconnect(worker) { 59 | Object.keys(self._workQueues).forEach(function(key) { 60 | self._workQueues[key].popWorker(worker); 61 | }); 62 | }; 63 | 64 | cluster.on('fork', self._onFork); 65 | cluster.on('disconnect', self._onDisconnect); 66 | 67 | self._final = _final; 68 | self.getWorkQueue = getWorkQueue; 69 | self.getTopicQueue = getTopicQueue; 70 | } 71 | 72 | // Not normally called, but we might need something like it in tests 73 | // to reset state back to initial. 74 | function _final() { 75 | var self = this; 76 | 77 | cluster.removeListener('fork', self._onFork); 78 | cluster.removeListener('disconnect', self._onDisconnect); 79 | } 80 | 81 | // 82 | // ## _getWorkQueue `_getWorkQueue(name)` 83 | // 84 | // Internal use only. 85 | // 86 | // Returns the WorkQueue named **name**, creating one if it doesn't already 87 | // exist. 88 | // 89 | function getWorkQueue(name) { 90 | var self = this; 91 | var queue = self._workQueues[name]; 92 | 93 | if (!queue) { 94 | queue = self._workQueues[name] = new WorkQueue(); 95 | } 96 | 97 | return queue; 98 | } 99 | 100 | // 101 | // ## _getTopicQueue `_getTopicQueue(name)` 102 | // 103 | // Internal use only. 104 | // 105 | // Returns the TopicQueue named **name**, creating one if it doesn't already 106 | // exist. 107 | // 108 | function getTopicQueue(name) { 109 | var self = this; 110 | var queue = self._topicQueues[name]; 111 | 112 | if (!queue) { 113 | // XXX(sam) topic queues should know subscribers and their topics 114 | queue = self._topicQueues[name] = new EventEmitter(); 115 | queue.publish = function(msg, topic) { 116 | // XXX(sam) sends to all workers, should only send to ones who subscribe 117 | // XXX(sam) send same obj to all workers, instead of recreating 118 | Object.keys(cluster.workers).forEach(function(id) { 119 | cluster.workers[id].send({ 120 | name: name, 121 | type: 'publish', 122 | topic: String(topic), 123 | msg: msg 124 | }); 125 | }); 126 | 127 | queue.emit('publish', { 128 | topic: String(topic), // XXX(sam) why String? 129 | msg: msg 130 | }); 131 | }; 132 | } 133 | 134 | return queue; 135 | } 136 | -------------------------------------------------------------------------------- /test/fixtures/manager.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // 7 | // # Manager 8 | // 9 | // A very simple manager for setting up and running distributed test cases. 10 | // 11 | // It also reads and returns results from the tmpfile. 12 | // 13 | // XXX(sam) These behaviours could be split into two, they don't depend on each 14 | // other, other than knowing the filename. 15 | var cluster = require('cluster'); 16 | var fs = require('fs'); 17 | var slmq = require('../../'); 18 | 19 | // 20 | // ## Manager `Manager(obj)` 21 | // 22 | // Creates a new instance of Manager. 23 | // 24 | function Manager(obj) { 25 | if (!(this instanceof Manager)) { 26 | return new Manager(obj); 27 | } 28 | 29 | obj = obj || { provider: 'native' }; 30 | 31 | // Only do in .init(), not needed in mocha 32 | this.connection = slmq.create(obj); 33 | this.connection.open(); 34 | 35 | this.filename = obj.filename || './out.txt'; 36 | } 37 | Manager.createManager = Manager; 38 | 39 | // 40 | // ##.init .init([workers])` 41 | // 42 | // Initializes the current process for testing and forks **workers** (defaults 43 | // to 2) child processes. 44 | // 45 | Manager.prototype.init = init; 46 | function init(workers) { 47 | var self = this; 48 | var count = typeof workers === 'number' ? workers : 2; 49 | 50 | if (cluster.isMaster) { 51 | fs.writeFileSync(self.filename, ''); 52 | 53 | for (var i = 0; i < Number(count); i++) { 54 | cluster.fork({ 55 | id: 'worker' + i, 56 | OUTFILE: self.filename 57 | }); 58 | } 59 | 60 | process.env.id = 'master'; 61 | } 62 | 63 | return self; 64 | } 65 | 66 | // 67 | // ## runTestPush `runTestPush(name, [messages])` 68 | // 69 | // Creates a PushQueue named **name**, publishing **messages** (defaults to 4) 70 | // messages over that queue. 71 | // 72 | Manager.prototype.runTestPush = runTestPush; 73 | function runTestPush(name, messages) { 74 | var self = this; 75 | var count = typeof messages === 'number' ? messages : 4; 76 | var queue = self.connection.createPushQueue(name); 77 | 78 | for (var i = 0; i < count; i++) { 79 | queue.publish(process.env.id + '.' + i); 80 | } 81 | 82 | return self; 83 | } 84 | 85 | // 86 | // ## runTestPull `runTestPull(name)` 87 | // 88 | // Creates a PullQueue named **name**, subscribing to all messages sent to it. 89 | // 90 | Manager.prototype.runTestPull = runTestPull; 91 | function runTestPull(name) { 92 | var self = this; 93 | var queue = self.connection.createPullQueue(name); 94 | 95 | queue.subscribe(function handler(msg) { 96 | fs.appendFileSync(self.filename, 97 | process.env.id + ':' + name + ':' + msg + '\n'); 98 | }); 99 | 100 | return self; 101 | } 102 | 103 | // 104 | // ## runTestPublish `runTestPublish(name, topic, [messages])` 105 | // 106 | // Creates a PublishQueue named **name**, publishing **messages** (defaults to 107 | // 4) messages over that queue. 108 | // 109 | Manager.prototype.runTestPublish = runTestPublish; 110 | function runTestPublish(name, topic, messages) { 111 | var self = this; 112 | var count = typeof messages === 'number' ? messages : 4; 113 | var queue = self.connection.createPubQueue(name); 114 | 115 | for (var i = 0; i < count; i++) { 116 | queue.publish(process.env.id + '.' + i, topic); 117 | } 118 | 119 | return self; 120 | } 121 | 122 | // 123 | // ## runTestSubscribe `runTestSubscribe(name, topic)` 124 | // 125 | // Creates a SubscribeQueue named **name**, subscribing to **topic** messages 126 | // sent to it. 127 | // 128 | Manager.prototype.runTestSubscribe = runTestSubscribe; 129 | function runTestSubscribe(name, topic) { 130 | var self = this; 131 | var queue = self.connection.createSubQueue(name); 132 | 133 | // During tests, workers do a lot of process.on('message'), this is expected. 134 | if (!cluster.isMaster) { 135 | process.setMaxListeners(20); 136 | } 137 | 138 | queue.subscribe(topic, function handler(msg) { 139 | fs.appendFileSync(self.filename, 140 | process.env.id + ':' + name + '.' + topic + ':' + msg + '\n'); 141 | }); 142 | 143 | return self; 144 | } 145 | 146 | // 147 | // ## loadTestResults `loadTestResults()` 148 | // 149 | // Loads all content printed by previous test runs, returning a sorted Array of 150 | // the results. 151 | // 152 | // Each line looks like: 153 | // ID:NAME:MSG 154 | // 155 | // ID is id of receiver of MSG (master, worker0, etc.) 156 | // NAME is name of test, as passed to manager.runTestXxx(NAME, ...) 157 | // MSG is the message received 158 | Manager.prototype.loadTestResults = loadTestResults; 159 | function loadTestResults() { 160 | var self = this; 161 | 162 | return fs 163 | .readFileSync(self.filename) 164 | .toString() 165 | .split('\n') 166 | .filter(function (line) { 167 | return !!line; 168 | }) 169 | .sort(); 170 | } 171 | 172 | module.exports = Manager; 173 | -------------------------------------------------------------------------------- /test/clusters.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | var assert = require('assert'); 7 | var os = require('os'); 8 | var path = require('path'); 9 | var fork = require('child_process').fork; 10 | var slmq = require('../'); 11 | var Manager = require('./fixtures/manager'); 12 | 13 | 14 | describe('clusters', function() { 15 | [ 16 | 'native', 17 | 'amqp', 18 | ].forEach(function(provider) { 19 | describe('with ' + provider, function() { 20 | // Fork child process, and run fixtures/harness. All tests are run by 21 | // harness before any of the it() functions below run, to assert the 22 | // results are as expected. 23 | // Since this all the tests running, it can take a while. 24 | this.timeout(30000); 25 | before(function(done) { 26 | var filename = path.join(os.tmpDir(), 'slmq-' + provider + '-test'); 27 | var manager = Manager.createManager({ 28 | provider: provider, 29 | filename: filename 30 | }); 31 | var results = this.results = { 32 | length: 0 33 | }; 34 | 35 | fork(require.resolve('./fixtures/harness'), [], { 36 | env: { 37 | provider: provider, 38 | filename: filename 39 | } 40 | }).on('exit', function() { 41 | // XXX(sam) manager knows format, it should create this data structure 42 | manager.loadTestResults().forEach(function(line) { 43 | var split = line.split(':'); 44 | 45 | assert.equal(split.length, 3, 'Malformed message: ' + line); 46 | 47 | // Map: 48 | // ID:NAME:MSG 49 | // to 50 | // results { 51 | // ID: { 52 | // NAME: [ 53 | // MSG, ... 54 | // ] 55 | // } 56 | // } 57 | 58 | results[split[0]] = results[split[0]] || {}; 59 | results[split[0]][split[1]] = results[split[0]][split[1]] || []; 60 | results[split[0]][split[1]].push(split[2]); 61 | results.length++; 62 | }); 63 | 64 | done(); 65 | }); 66 | 67 | // Assert that `recipient` received `length` number of `name` messages. 68 | this.checkMessageArray = function(recipient, name, length) { 69 | var array = results[recipient.toLowerCase()][name]; 70 | assert(array, 71 | recipient + ' did not receive "' + name + '" messages.'); 72 | 73 | var delta = length - array.length; 74 | assert.equal(delta, 0, 75 | delta + '"' + name + 76 | '" messages were dropped heading to ' + recipient + '.'); 77 | }; 78 | }); 79 | 80 | it('should send all messages', function() { 81 | assert.equal(this.results.length, 240, 82 | (this.results.length - 240) + ' messages were dropped.'); 83 | }); 84 | 85 | it('should send messages to all processes', function() { 86 | assert(this.results.master, 'Master did not receive messages'); 87 | assert(this.results.worker0, 'Worker0 did not receive messages'); 88 | assert(this.results.worker1, 'Worker1 did not receive messages'); 89 | }); 90 | 91 | it('should filter work queues by name', function() { 92 | this.checkMessageArray('Master', 'master.work', 12); 93 | this.checkMessageArray('Master', 'all.work', 4); 94 | 95 | this.checkMessageArray('Worker0', 'worker0.work', 12); 96 | this.checkMessageArray('Worker0', 'workers.work', 6); 97 | this.checkMessageArray('Worker0', 'all.work', 4); 98 | 99 | this.checkMessageArray('Worker1', 'worker1.work', 12); 100 | this.checkMessageArray('Worker1', 'workers.work', 6); 101 | this.checkMessageArray('Worker1', 'all.work', 4); 102 | }); 103 | 104 | it('should support PushQueue first or PullQueue first', function() { 105 | this.checkMessageArray('Master', 'master.pushfirst', 4); 106 | this.checkMessageArray('Master', 'master.pullfirst', 4); 107 | 108 | this.checkMessageArray('Worker0', 'worker0.pushfirst', 4); 109 | this.checkMessageArray('Worker0', 'worker0.pullfirst', 4); 110 | 111 | this.checkMessageArray('Worker1', 'worker1.pushfirst', 4); 112 | this.checkMessageArray('Worker1', 'worker1.pullfirst', 4); 113 | }); 114 | 115 | it('should filter topic queues by name', function() { 116 | this.checkMessageArray('Master', 'master.topic.test', 12); 117 | this.checkMessageArray('Worker0', 'worker0.topic.test', 12); 118 | this.checkMessageArray('Worker1', 'worker1.topic.test', 12); 119 | 120 | this.checkMessageArray('Worker0', 'workers.topic.test', 12); 121 | this.checkMessageArray('Worker1', 'workers.topic.test', 12); 122 | 123 | this.checkMessageArray('Master', 'all.topic.test', 12); 124 | this.checkMessageArray('Worker0', 'all.topic.test', 12); 125 | this.checkMessageArray('Worker1', 'all.topic.test', 12); 126 | }); 127 | 128 | it('should filter subscriptions by topic', function() { 129 | this.checkMessageArray('Master', 'all.topic.master', 12); 130 | this.checkMessageArray('Worker0', 'all.topic.worker0', 12); 131 | this.checkMessageArray('Worker1', 'all.topic.worker1', 12); 132 | 133 | this.checkMessageArray('Worker0', 'workers.topic.worker0', 12); 134 | this.checkMessageArray('Worker1', 'workers.topic.worker1', 12); 135 | }); 136 | }); 137 | }); 138 | }); 139 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ### Contributing ### 2 | 3 | Thank you for your interest in `strong-mq`, an open source project 4 | administered by StrongLoop. 5 | 6 | Contributing to `strong-mq` is easy. In a few simple steps: 7 | 8 | * Ensure that your effort is aligned with the project's roadmap by 9 | talking to the maintainers, especially if you are going to spend a 10 | lot of time on it. 11 | 12 | * Make something better or fix a bug. 13 | 14 | * Adhere to code style outlined in the [Google C++ Style Guide][] and 15 | [Google Javascript Style Guide][]. 16 | 17 | * Sign the [Contributor License Agreement](https://cla.strongloop.com/agreements/strongloop/strong-mq) 18 | 19 | * Submit a pull request through Github. 20 | 21 | 22 | ### Contributor License Agreement ### 23 | 24 | ``` 25 | Individual Contributor License Agreement 26 | 27 | By signing this Individual Contributor License Agreement 28 | ("Agreement"), and making a Contribution (as defined below) to 29 | StrongLoop, Inc. ("StrongLoop"), You (as defined below) accept and 30 | agree to the following terms and conditions for Your present and 31 | future Contributions submitted to StrongLoop. Except for the license 32 | granted in this Agreement to StrongLoop and recipients of software 33 | distributed by StrongLoop, You reserve all right, title, and interest 34 | in and to Your Contributions. 35 | 36 | 1. Definitions 37 | 38 | "You" or "Your" shall mean the copyright owner or the individual 39 | authorized by the copyright owner that is entering into this 40 | Agreement with StrongLoop. 41 | 42 | "Contribution" shall mean any original work of authorship, 43 | including any modifications or additions to an existing work, that 44 | is intentionally submitted by You to StrongLoop for inclusion in, 45 | or documentation of, any of the products owned or managed by 46 | StrongLoop ("Work"). For purposes of this definition, "submitted" 47 | means any form of electronic, verbal, or written communication 48 | sent to StrongLoop or its representatives, including but not 49 | limited to communication or electronic mailing lists, source code 50 | control systems, and issue tracking systems that are managed by, 51 | or on behalf of, StrongLoop for the purpose of discussing and 52 | improving the Work, but excluding communication that is 53 | conspicuously marked or otherwise designated in writing by You as 54 | "Not a Contribution." 55 | 56 | 2. You Grant a Copyright License to StrongLoop 57 | 58 | Subject to the terms and conditions of this Agreement, You hereby 59 | grant to StrongLoop and recipients of software distributed by 60 | StrongLoop, a perpetual, worldwide, non-exclusive, no-charge, 61 | royalty-free, irrevocable copyright license to reproduce, prepare 62 | derivative works of, publicly display, publicly perform, 63 | sublicense, and distribute Your Contributions and such derivative 64 | works under any license and without any restrictions. 65 | 66 | 3. You Grant a Patent License to StrongLoop 67 | 68 | Subject to the terms and conditions of this Agreement, You hereby 69 | grant to StrongLoop and to recipients of software distributed by 70 | StrongLoop a perpetual, worldwide, non-exclusive, no-charge, 71 | royalty-free, irrevocable (except as stated in this Section) 72 | patent license to make, have made, use, offer to sell, sell, 73 | import, and otherwise transfer the Work under any license and 74 | without any restrictions. The patent license You grant to 75 | StrongLoop under this Section applies only to those patent claims 76 | licensable by You that are necessarily infringed by Your 77 | Contributions(s) alone or by combination of Your Contributions(s) 78 | with the Work to which such Contribution(s) was submitted. If any 79 | entity institutes a patent litigation against You or any other 80 | entity (including a cross-claim or counterclaim in a lawsuit) 81 | alleging that Your Contribution, or the Work to which You have 82 | contributed, constitutes direct or contributory patent 83 | infringement, any patent licenses granted to that entity under 84 | this Agreement for that Contribution or Work shall terminate as 85 | of the date such litigation is filed. 86 | 87 | 4. You Have the Right to Grant Licenses to StrongLoop 88 | 89 | You represent that You are legally entitled to grant the licenses 90 | in this Agreement. 91 | 92 | If Your employer(s) has rights to intellectual property that You 93 | create, You represent that You have received permission to make 94 | the Contributions on behalf of that employer, that Your employer 95 | has waived such rights for Your Contributions, or that Your 96 | employer has executed a separate Corporate Contributor License 97 | Agreement with StrongLoop. 98 | 99 | 5. The Contributions Are Your Original Work 100 | 101 | You represent that each of Your Contributions are Your original 102 | works of authorship (see Section 8 (Submissions on Behalf of 103 | Others) for submission on behalf of others). You represent that to 104 | Your knowledge, no other person claims, or has the right to claim, 105 | any right in any intellectual property right related to Your 106 | Contributions. 107 | 108 | You also represent that You are not legally obligated, whether by 109 | entering into an agreement or otherwise, in any way that conflicts 110 | with the terms of this Agreement. 111 | 112 | You represent that Your Contribution submissions include complete 113 | details of any third-party license or other restriction (including, 114 | but not limited to, related patents and trademarks) of which You 115 | are personally aware and which are associated with any part of 116 | Your Contributions. 117 | 118 | 6. You Don't Have an Obligation to Provide Support for Your Contributions 119 | 120 | You are not expected to provide support for Your Contributions, 121 | except to the extent You desire to provide support. You may provide 122 | support for free, for a fee, or not at all. 123 | 124 | 6. No Warranties or Conditions 125 | 126 | StrongLoop acknowledges that unless required by applicable law or 127 | agreed to in writing, You provide Your Contributions on an "AS IS" 128 | BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, EITHER 129 | EXPRESS OR IMPLIED, INCLUDING, WITHOUT LIMITATION, ANY WARRANTIES 130 | OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY, OR 131 | FITNESS FOR A PARTICULAR PURPOSE. 132 | 133 | 7. Submission on Behalf of Others 134 | 135 | If You wish to submit work that is not Your original creation, You 136 | may submit it to StrongLoop separately from any Contribution, 137 | identifying the complete details of its source and of any license 138 | or other restriction (including, but not limited to, related 139 | patents, trademarks, and license agreements) of which You are 140 | personally aware, and conspicuously marking the work as 141 | "Submitted on Behalf of a Third-Party: [named here]". 142 | 143 | 8. Agree to Notify of Change of Circumstances 144 | 145 | You agree to notify StrongLoop of any facts or circumstances of 146 | which You become aware that would make these representations 147 | inaccurate in any respect. Email us at callback@strongloop.com. 148 | ``` 149 | 150 | [Google C++ Style Guide]: https://google.github.io/styleguide/cppguide.html 151 | [Google Javascript Style Guide]: https://google.github.io/styleguide/javascriptguide.xml 152 | -------------------------------------------------------------------------------- /api.md: -------------------------------------------------------------------------------- 1 | ## Message Patterns 2 | 3 | - work queue: published messages are delivered to a single subscriber, common 4 | when distributing work items that should be processed by a single worker 5 | - topic: published messages are delivered to all subscribers, each message is 6 | associated with a "topic", and subscribers can specify the topic patterns they 7 | want to receive 8 | - rpc: published messages are delivered to a single subscriber, and a associated 9 | response is returned to the original publisher (TBD) 10 | 11 | ## Event: 'error' 12 | 13 | Errors may be emitted as events from either a connection or a queue. The nature of the 14 | errors emitted depends on the underlying provider. 15 | 16 | ## Connections 17 | 18 | ### slmq.create([options|url]) 19 | 20 | Returns a connection object for a specific provider, configuration can 21 | be created using a options object, or a url: 22 | 23 | * `options` {Object} 24 | * `url` {provider://...} 25 | 26 | If `create()` is called with no arguments, the native provider will be used. 27 | 28 | Supported providers are: 29 | 30 | * `'amqp'`: RabbitMQ 31 | * `'native'`: Cluster-native messaging 32 | 33 | Supported options, other than `provider`, depend on the provider: 34 | 35 | * `provider` {String} Mandatory name of provider, such as `'amqp'` 36 | * `host` {String} Name of host to connect to (if supported by provider) 37 | * `port` {String} Port to connect to (if supported by provider) 38 | * `...` As supported by the provider 39 | 40 | Example of creating an amqp connection, using an options object: 41 | 42 | connection = clustemq.create({ 43 | provider: 'amqp', 44 | host: 'localhost', 45 | user: 'guest', 46 | }); 47 | 48 | Example of declaring amqp, using a URL: 49 | 50 | connection = clusermq.create('amqp://guest@localhost'); 51 | 52 | ### connection.provider {String} 53 | 54 | Property is set to the name of the provider. 55 | 56 | 57 | ### connection.open() 58 | 59 | Opens a connection. 60 | 61 | Example: 62 | 63 | connection.open().on('error', function () { 64 | // ... handle error 65 | }); 66 | 67 | ### connection.close([callback]) 68 | 69 | Callsback when connection has been closed. 70 | 71 | ## Work queues (push/pull) 72 | 73 | ### Queue life-time 74 | 75 | Queues are closed when they are empty and have no users. They might or might not 76 | be persistent across restarts of the queue broker, depending on the provider. 77 | 78 | ### Messages 79 | 80 | Message objects can be either an `Object` or `Array`, transmitted as JSON, or a `String` 81 | or `Buffer`, transmitted as data. 82 | 83 | ### connection.createPushQueue(queue_name) 84 | 85 | Return a queue for publishing work items. 86 | 87 | ### push.publish(msg) 88 | 89 | Publish a msg to a push queue. 90 | 91 | * `msg` {Object} Message to publish to the queue 92 | 93 | ### connection.createPullQueue(queue_name) 94 | 95 | Return a queue for subscribing to work items. 96 | 97 | ### pull.subscribe([listener]) 98 | 99 | Listen for messages on a work queue. 100 | 101 | `listener` is optional, it will be added as a listener 102 | for the `'message'` event if provided. 103 | 104 | ### queue.close() 105 | 106 | Close the queue. 107 | 108 | ### queue.name {String} 109 | 110 | Name used to create queue. 111 | 112 | ### queue.type {String} 113 | 114 | Either 'push', or 'pull'. 115 | 116 | ### Event: 'message' 117 | 118 | Event is emitted when a subcribed pull queue receives a message. 119 | 120 | * `msg` {Object} Message pulled off the queue 121 | 122 | ## Topic queue (pub/sub) 123 | 124 | Topics are dot-separated alphanumeric (or `'_'`) words. Subscription patterns match 125 | leading words. 126 | 127 | ### connection.createPubQueue(queue_name) 128 | 129 | Return a queue for publishing on topics. 130 | 131 | ### pub.publish(msg, topic) 132 | 133 | * `msg` {Object} Message to publish onto the queue 134 | * `topic` {String} Topic of message, default is `''` 135 | 136 | ### connection.createSubQueue(queue_name) 137 | 138 | Return a queue for subscribing to topics. 139 | 140 | ### sub.subscribe(pattern[, listener]) 141 | 142 | Listen for messages matching pattern on a topic queue. 143 | 144 | * `pattern` {String} Pattern of message, may contain wildcards, default is `''` 145 | 146 | `listener` is optional, it will be added as a listener for the `'message'` event 147 | if provided. Add your listener to the `'message'` event directly when 148 | subscribing multiple times, or all your listeners will be called for all 149 | messages. 150 | 151 | Example of subscribing to multiple patterns: 152 | 153 | sub.subscribe('that.*') 154 | .subscribe('this.*') 155 | .on('message', function (msg) { ... }); 156 | 157 | Example of subscribing to a single pattern, and providing a listener: 158 | 159 | sub.subscribe('other.*', function (msg) { ... }); 160 | 161 | ### queue.close() 162 | 163 | Close the queue. 164 | 165 | ### queue.name {String} 166 | 167 | Name used to create queue. 168 | 169 | ### queue.type {String} 170 | 171 | Either 'pub', or 'sub'. 172 | 173 | ### Event: 'message' 174 | 175 | Event is emitted when a subcribed pull queue receives a message. 176 | 177 | * `msg` {Object} Message pulled off the queue 178 | 179 | ## Provider: NATIVE 180 | 181 | The NativeConnection uses the built-in 182 | [cluster](http://nodejs.org/docs/latest/api/cluster.html) module to facilitate 183 | the strong-mq API. It's designed to be the first adapter people use in early 184 | development, before they get whatever system they will use for deployment up and 185 | running. 186 | 187 | It has no options. 188 | 189 | The URL format is: 190 | 191 | native:[//] 192 | 193 | ## Provider: AMQP 194 | 195 | Support for RabbitMQ using the AMQP protocol. This provider is based 196 | on the [node-amqp](https://www.npmjs.org/package/amqp) module, see 197 | its documentation for more information. 198 | 199 | The options (except for `.provider`) or url is passed directly to node-amqp, 200 | supported options are: 201 | 202 | * `host` {String} Hostname to connect to, defaults to `'localhost'` 203 | * `port` {String} Port to connect to, defaults to `5672` 204 | * `login` {String} Username to authenticate as, defaults to `'guest'` 205 | * `password` {String} Password to authenticate as, defaults to `'guest'` 206 | * `vhost` {String} Vhost, defaults to `'/'` 207 | 208 | The URL format for specifying the options above is: 209 | 210 | amqp://[login][:password][@]host[:port][/vhost] 211 | 212 | Note that the `host` is mandatory when using a URL. 213 | 214 | Note that node-amqp supports RabbitMQ 3.0.4, or higher. In particular, it will 215 | *not* work with RabbitMQ 1.8.1 that is packaged with Debian 6, see the [upgrade 216 | instructions](http://www.rabbitmq.com/install-debian.html). 217 | 218 | ## Provider: STOMP 219 | 220 | Support for ActiveMQ using the STOMP protocol. This provider is based on the 221 | [node-stomp-client](https://github.com/easternbloc/node-stomp-client) module. 222 | 223 | The options are: 224 | 225 | * `host` {String} Hostname to connect to, defaults to `'127.0.0.1'` 226 | * `port` {String} Port to connect to, defaults to `61613` 227 | * `login` {String} Username to authenticate as, defaults to none 228 | * `password` {String} Password to authenticate as, defaults to none 229 | 230 | The URL format for specifying the options above is: 231 | 232 | stomp://[login][:password][@]host[:port] 233 | 234 | Note that the `host` is mandatory when using a URL. 235 | 236 | ActiveMQ ships with an example configuration sufficient to run the strong-mq 237 | unit tests. 238 | 239 | Note that node-stomp-client has been tested only with Active MQ 5.8.0. It can be 240 | installed from [apache](http://activemq.apache.org/activemq-580-release.html), 241 | and run as: 242 | 243 | activemq console xbean:activemq-stomp.xml 244 | -------------------------------------------------------------------------------- /lib/adapters/stomp.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // Provider: STOMP 7 | 8 | module.exports = CreateStomp; 9 | 10 | var stomp = require('stomp-client'); 11 | var assert = require('assert'); 12 | var dbg = require('../dbg'); 13 | var events = require('events'); 14 | var jobs = require('../jobs'); 15 | var checkTopic = require('../topic').check; 16 | var util = require('util'); 17 | var parse = require('url').parse; 18 | 19 | function forwardEvent(name, from, to) 20 | { 21 | from.on(name, to.emit.bind(to, name)); 22 | } 23 | 24 | 25 | //-- Connection 26 | 27 | function CreateStomp(provider, url, options) { 28 | var self = this; 29 | options = url ? parseUrl(url) : options; 30 | 31 | self.provider = provider; 32 | self._stomp = new stomp.StompClient( 33 | options.host, 34 | options.port, 35 | options.login, 36 | options.password, 37 | options.protocolVersion 38 | ); 39 | self._whenReady = jobs.delayed(); 40 | 41 | forwardEvent('error', self._stomp, self); 42 | } 43 | 44 | util.inherits(CreateStomp, events.EventEmitter); 45 | 46 | function parseUrl(url) { 47 | var parts = parse(url, false, true); // no query, yes extract host 48 | var options = { 49 | host: parts.hostname, 50 | port: parts.port, 51 | }; 52 | if (parts.auth) { 53 | var auth = parts.auth.split(':'); 54 | options.login = auth[0]; 55 | options.password = auth[1]; 56 | } 57 | return options; 58 | } 59 | 60 | CreateStomp._parseUrl = parseUrl; // expose to unit tests 61 | 62 | 63 | CreateStomp.prototype.open = function() { 64 | var self = this; 65 | 66 | self._stomp.connect(function() { 67 | dbg('task-start'); 68 | self._whenReady.start(); 69 | }); 70 | return this; 71 | }; 72 | 73 | CreateStomp.prototype._doWhenReady = function(callback) { 74 | dbg('task-queue:', callback.name); 75 | assert(callback.name); 76 | this._whenReady.push(callback, function() { 77 | dbg('task-done:', callback.name); 78 | }); 79 | return this; 80 | }; 81 | 82 | CreateStomp.prototype.close = function(callback) { 83 | var self = this; 84 | 85 | self._doWhenReady(function connClose(done) { 86 | dbg('connection close start'); 87 | self._stomp.disconnect(function() { 88 | dbg('connection close done'); 89 | done(); 90 | if (callback) { 91 | callback(); 92 | } 93 | }); 94 | }); 95 | 96 | return self; 97 | }; 98 | 99 | //-- Common utilities 100 | 101 | // Get connection for a queue 102 | function c(q) { 103 | return q._connection; 104 | } 105 | 106 | // Get stomp object for a queue 107 | function s(q) { 108 | return c(q)._stomp; 109 | } 110 | 111 | // stomp doesn't require queues to be opened or closed, your just publish to 112 | // a name (either /queue/ for tasks or /topic/ for topics). 113 | var PREFIX = { 114 | push: 'queue', 115 | pull: 'queue', 116 | pub: 'topic', 117 | sub: 'topic', 118 | }; 119 | 120 | function stompOpen(self, type, connection, name) { 121 | var prefix = PREFIX[type]; 122 | assert(prefix); 123 | 124 | self.name = name; 125 | self.type = type; 126 | self._connection = connection; 127 | self._q = '/'+prefix+'/'+name; 128 | dbg('queue opened', self._q); 129 | } 130 | 131 | function stompClose() { 132 | var self = this; 133 | self._q = null; 134 | return self; 135 | } 136 | 137 | function topicSelector(pattern) { 138 | pattern = checkTopic(pattern); 139 | 140 | // A pattern of '' should match any topic. In this case, we won't even 141 | // test for the topic key header, allowing a msg that has no topic 142 | // at all to still match the '' pattern. 143 | 144 | if (pattern == null || pattern === '') { 145 | return null; 146 | } 147 | 148 | return 'topickey LIKE \''+pattern+'.%\' OR topickey = \''+pattern+'\''; 149 | } 150 | 151 | function stompSubscribe(self, pattern, callback) { 152 | var selector = topicSelector(pattern); 153 | 154 | if (callback) { 155 | self.on('message', callback); 156 | } 157 | 158 | c(self)._doWhenReady(function pullSubscribe(done) { 159 | var headers = {}; 160 | 161 | dbg('pull subscribe start+done'); 162 | 163 | if (selector) { 164 | headers.selector = selector; 165 | } 166 | 167 | s(self).subscribe(self._q, headers, function(msg, headers) { 168 | dbg('pull subscribe callback', msg, headers); 169 | try { 170 | msg = decode(msg, headers['content-type']); 171 | self.emit('message', msg); 172 | } catch(er) { 173 | er.queue = self._q; 174 | er.type = self.type; 175 | er.source = 'subscribe'; 176 | self.emit('error', er); 177 | } 178 | }); 179 | done(); 180 | }); 181 | return self; 182 | } 183 | 184 | // either encode msg as json, and return encoded msg and content-type value, 185 | // or use string value and return null content-type 186 | // @return [contentType, buffer] 187 | // Note: implementation ripped out of node-amqp 188 | // 189 | // XXX should this be merged to STOMP? if so, could be optional, triggered 190 | // only if body is not a string/buffer, and if header has no content-type. 191 | function encode(body) { 192 | // Handles 3 cases 193 | // - body is utf8 string 194 | // - body is instance of Buffer 195 | // - body is an object and its JSON representation is sent 196 | // Does not handle the case for streaming bodies. 197 | // Returns buffer. 198 | if (typeof(body) == 'string') { 199 | return [new Buffer(body, 'utf8')]; 200 | } else if (body instanceof Buffer) { 201 | return [body]; 202 | } else { 203 | var jsonBody = JSON.stringify(body); 204 | return [new Buffer(jsonBody, 'utf8'), 'application/json']; 205 | } 206 | } 207 | 208 | // return either an object/buffer/string, throws! 209 | function decode(body, contentType) { 210 | if (contentType !== 'application/json') { 211 | return body; 212 | } 213 | return JSON.parse(body); 214 | } 215 | 216 | function stompPublish(self, msg, topic) { 217 | c(self)._doWhenReady(function pushPublish(done) { 218 | var headers = {}; 219 | 220 | dbg(self.type+' publish start', msg); 221 | var encoding = encode(msg); 222 | if(encoding[1]) { 223 | headers['content-type'] = encoding[1]; 224 | } 225 | if (topic) { 226 | topic = checkTopic(topic); 227 | headers.topickey = topic; // XXX see if I can use topic-key in the selector 228 | } 229 | s(self).publish(self._q, encoding[0], headers); 230 | //XXX(sam) callback will only happen if we implement acknowledgement 231 | // in the stomp client, for now, just have to say done in next tick. 232 | process.nextTick(function() { 233 | dbg(self.type+' publish HOPEFULLY done'); 234 | done(); 235 | }); 236 | }); 237 | return self; 238 | } 239 | 240 | 241 | //-- Push/Pull Queue 242 | 243 | function PushStomp(connection, name) { 244 | stompOpen(this, 'push', connection, name); 245 | } 246 | 247 | util.inherits(PushStomp, events.EventEmitter); 248 | 249 | PushStomp.prototype.publish = function(msg) { 250 | return stompPublish(this, msg); 251 | }; 252 | 253 | PushStomp.prototype.close = stompClose; 254 | 255 | CreateStomp.prototype.createPushQueue = function(name) { 256 | return new PushStomp(this, name); 257 | }; 258 | 259 | function PullStomp(connection, name) { 260 | stompOpen(this, 'pull', connection, name); 261 | } 262 | 263 | util.inherits(PullStomp, events.EventEmitter); 264 | 265 | PullStomp.prototype.subscribe = function(callback) { 266 | return stompSubscribe(this, null, callback); 267 | }; 268 | 269 | PullStomp.prototype.close = stompClose; 270 | 271 | CreateStomp.prototype.createPullQueue = function(name, callback) { 272 | return new PullStomp(this, name, callback); 273 | }; 274 | 275 | 276 | //-- Pub/Sub Queue 277 | 278 | function PubStomp(connection, name) { 279 | stompOpen(this, 'pub', connection, name); 280 | } 281 | 282 | util.inherits(PubStomp, events.EventEmitter); 283 | 284 | PubStomp.prototype.publish = function(msg, topic) { 285 | topic = checkTopic(topic); 286 | return stompPublish(this, msg, topic); 287 | }; 288 | 289 | PubStomp.prototype.close = stompClose; 290 | 291 | CreateStomp.prototype.createPubQueue = function(name) { 292 | return new PubStomp(this, name); 293 | }; 294 | 295 | function SubStomp(connection, name) { 296 | stompOpen(this, 'sub', connection, name); 297 | } 298 | 299 | util.inherits(SubStomp, events.EventEmitter); 300 | 301 | SubStomp.prototype.subscribe = function(pattern, callback) { 302 | return stompSubscribe(this, pattern, callback); 303 | }; 304 | 305 | SubStomp.prototype.close = stompClose; 306 | 307 | CreateStomp.prototype.createSubQueue = function(name) { 308 | return new SubStomp(this, name); 309 | }; 310 | -------------------------------------------------------------------------------- /lib/adapters/amqp.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // Provider: AMQP 7 | 8 | module.exports = CreateAmqp; 9 | 10 | var amqp = require('amqp'); 11 | var assert = require('assert'); 12 | var dbg = require('../dbg'); 13 | var events = require('events'); 14 | var jobs = require('../jobs'); 15 | var checkTopic = require('../topic').check; 16 | var util = require('util'); 17 | 18 | function forwardEvent(name, from, to) 19 | { 20 | from.on(name, to.emit.bind(to, name)); 21 | } 22 | 23 | 24 | //-- Connection 25 | 26 | function CreateAmqp(provider, url, options) { 27 | var self = this; 28 | options = url ? {url: url} : options; 29 | amqp.Connection.call(self, options, {reconnect: false}); 30 | 31 | self.provider = provider; 32 | 33 | // slmq private extensions to node-amqp connection 34 | self._slmq = { 35 | whenReady: jobs.delayed(), // delayed until connection ready 36 | }; 37 | 38 | self.once('ready', function() { 39 | dbg('task-start'); 40 | self._slmq.whenReady.start(); 41 | }); 42 | } 43 | 44 | util.inherits(CreateAmqp, amqp.Connection); 45 | 46 | CreateAmqp.prototype.open = function() { 47 | this.connect(); 48 | return this; 49 | }; 50 | 51 | CreateAmqp.prototype._doWhenReady = function(callback) { 52 | dbg('task-queue:', callback.name); 53 | assert(callback.name); 54 | this._slmq.whenReady.push(callback, function() { 55 | dbg('task-done:', callback.name); 56 | }); 57 | return this; 58 | }; 59 | 60 | CreateAmqp.prototype.close = function(callback) { 61 | var self = this; 62 | 63 | self._doWhenReady(function connClose(done) { 64 | dbg('connection close start'); 65 | //XXX(sam) don't think its clean to FIN a connection, amqp likes to see 66 | //an explicit close, sometimes self causes RST (but not always). 67 | self.end(); 68 | self.once('close', function() { 69 | dbg('connection close done'); 70 | done(); 71 | if (callback) { 72 | callback(); 73 | } 74 | }); 75 | }); 76 | 77 | return self; 78 | }; 79 | 80 | //-- Push/Pull Queue 81 | 82 | // Get amqp connection for a queue 83 | function c(q) { 84 | return q._connection; 85 | } 86 | 87 | // Common options when creating and destroying queues 88 | var CREATE_OPTIONS = { 89 | autoDelete: true 90 | }; 91 | 92 | // Using these options causes an error event to be emitted if the q is in use 93 | // or non-empty, so the autoDelete flag appears to be a better way. 94 | var DESTROY_OPTIONS = { 95 | //ifUnused: true, 96 | //ifEmpty: true, 97 | }; 98 | 99 | function amqpOpen(self, type, connection, name) { 100 | self.name = name; 101 | self.type = type; 102 | self._connection = connection; 103 | c(self)._doWhenReady(function amqpOpen(done) { 104 | dbg('queue open start', name); 105 | self._q = c(self).queue(name, CREATE_OPTIONS, function() { 106 | dbg('queue open done', name); 107 | done(); 108 | }); 109 | forwardEvent('error', self._q, self); 110 | }); 111 | } 112 | 113 | function amqpClose() { 114 | var self = this; 115 | 116 | c(self)._doWhenReady(function amqpClose(done) { 117 | if (self._q == null) { 118 | dbg('queue double close', self.type, self.name); 119 | done(); 120 | return; 121 | } 122 | dbg('queue close start', self.type, self.name); 123 | self._q.once('close', function() { 124 | dbg('queue close done', self.type, self.name); 125 | done(); 126 | }); 127 | self._q.close(); 128 | self._q = null; 129 | }); 130 | 131 | return self; 132 | } 133 | 134 | function PushAmqp(connection, name) { 135 | amqpOpen(this, 'push', connection, name); 136 | } 137 | 138 | util.inherits(PushAmqp, events.EventEmitter); 139 | 140 | PushAmqp.prototype.publish = function(msg) { 141 | var self = this; 142 | c(self)._doWhenReady(function pushPublish(done) { 143 | dbg('push publish start', msg); 144 | c(self).publish(self._q.name, msg, null, function() { 145 | }); //XXX(sam) callback will only happen if we force the 146 | // default exchange to have .confirm set, not sure how, maybe open an 147 | // exchange explicitly? this is sucky, but for now, just have to say done 148 | // in next tick 149 | process.nextTick(function() { 150 | dbg('push publish HOPEFULLY done'); 151 | done(); 152 | }); 153 | }); 154 | return self; 155 | }; 156 | 157 | PushAmqp.prototype.close = amqpClose; 158 | 159 | CreateAmqp.prototype.createPushQueue = function(name) { 160 | return new PushAmqp(this, name); 161 | }; 162 | 163 | function PullAmqp(connection, name) { 164 | amqpOpen(this, 'pull', connection, name); 165 | } 166 | 167 | util.inherits(PullAmqp, events.EventEmitter); 168 | 169 | PullAmqp.prototype.subscribe = function(callback) { 170 | var self = this; 171 | 172 | if (callback) { 173 | self.on('message', callback); 174 | } 175 | 176 | c(self)._doWhenReady(function pullSubscribe(done) { 177 | dbg('pull subscribe start+done'); 178 | self._q.subscribe(/* ack? prefetchCount? */ function(msg) { 179 | dbg('pull subscribe callback', msg); 180 | if (msg.data && msg.contentType) { 181 | msg = msg.data; // non-json 182 | } // else msg is already-parsed json 183 | self.emit('message', msg); 184 | }); 185 | done(); 186 | }); 187 | return self; 188 | }; 189 | 190 | PullAmqp.prototype.close = amqpClose; 191 | 192 | CreateAmqp.prototype.createPullQueue = function(name, callback) { 193 | return new PullAmqp(this, name, callback); 194 | }; 195 | 196 | 197 | //-- Pub/Sub Queue 198 | 199 | var EXCHANGE_OPTIONS = {autoDelete: true, type: 'topic'}; 200 | 201 | function PubAmqp(connection, name) { 202 | var self = this; 203 | self.name = name; 204 | self.type = 'pub'; 205 | self._connection = connection; 206 | c(self)._doWhenReady(function pubExchange(done) { 207 | self._q = c(self).exchange(name, EXCHANGE_OPTIONS, function () { 208 | done(); 209 | }); 210 | forwardEvent('error', self._q, self); 211 | }); 212 | } 213 | 214 | util.inherits(PubAmqp, events.EventEmitter); 215 | 216 | PubAmqp.prototype.publish = function(msg, topic) { 217 | var self = this; 218 | topic = checkTopic(topic); 219 | c(self)._doWhenReady(function pubPublish(done) { 220 | self._q.publish(topic, msg); 221 | done(); 222 | }); 223 | return self; 224 | }; 225 | 226 | PubAmqp.prototype.close = amqpClose; 227 | 228 | CreateAmqp.prototype.createPubQueue = function(name) { 229 | return new PubAmqp(this, name); 230 | }; 231 | 232 | function SubAmqp(connection, name) { 233 | var self = this; 234 | self.name = name; 235 | self.type = 'sub'; 236 | self._connection = connection; 237 | c(self)._doWhenReady(function subQueueOpen(done) { 238 | self._q = c(self).queue('', { 239 | autoDelete: true, exclusive: true 240 | }, function(q) { 241 | done(); 242 | }); 243 | forwardEvent('error', self._q, self); 244 | }); 245 | 246 | // We can subscribe here, because nothing will come from self queue until 247 | // it's bound to topics (with .subscribe(), below). 248 | connection._doWhenReady(function subSubscribe(done) { 249 | self._q.subscribe(function(msg) { 250 | if (msg.data && msg.contentType) { 251 | msg = msg.data; // non-json 252 | } // else msg is already-parsed json 253 | self.emit('message', msg); 254 | }); 255 | done(); 256 | }); 257 | 258 | c(self)._doWhenReady(function subExchange(done) { 259 | self._exchange = c(self).exchange(name, EXCHANGE_OPTIONS, function () { 260 | done(); 261 | }); 262 | forwardEvent('error', self._exchange, self); 263 | }); 264 | } 265 | 266 | util.inherits(SubAmqp, events.EventEmitter); 267 | 268 | SubAmqp.prototype.subscribe = function(pattern, callback) { 269 | pattern = checkTopic(pattern); 270 | 271 | // Append AMQP multi-word wildcard to pattern 272 | if (pattern === '') { 273 | pattern = '#'; 274 | } else { 275 | pattern = pattern + '.#'; 276 | } 277 | 278 | var self = this; 279 | 280 | if (callback) { 281 | self.on('message', callback); 282 | } 283 | 284 | c(self)._doWhenReady(function subBind(done) { 285 | dbg('sub subscribe start', pattern); 286 | self._q.bind(self.name, pattern); 287 | self._q.once('queueBindOk', function() { 288 | dbg('sub subscribe done'); 289 | done(); 290 | }); 291 | }); 292 | 293 | return self; 294 | }; 295 | 296 | SubAmqp.prototype.close = function subClose() { 297 | var self = this; 298 | 299 | // Close the queue, then the topic exchange. 300 | amqpClose.call(self); 301 | 302 | c(self)._doWhenReady(function subExchangeClose(done) { 303 | if (self._exchange == null) { 304 | done(); 305 | return; 306 | } 307 | dbg('exchange close start', self.type, self.name); 308 | self._exchange.once('close', function() { 309 | dbg('exchange close done', self.type, self.name); 310 | done(); 311 | }); 312 | self._exchange.close(); 313 | self._exchange = null; 314 | }); 315 | 316 | return self; 317 | }; 318 | 319 | CreateAmqp.prototype.createSubQueue = function(name) { 320 | return new SubAmqp(this, name); 321 | }; 322 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) IBM Corp. 2013,2015. All Rights Reserved. 2 | Node module: strong-mq 3 | This project is licensed under the Artistic License 2.0, full text below. 4 | 5 | -------- 6 | 7 | The Artistic License 2.0 8 | 9 | Copyright (c) 2000-2006, The Perl Foundation. 10 | 11 | Everyone is permitted to copy and distribute verbatim copies 12 | of this license document, but changing it is not allowed. 13 | 14 | Preamble 15 | 16 | This license establishes the terms under which a given free software 17 | Package may be copied, modified, distributed, and/or redistributed. 18 | The intent is that the Copyright Holder maintains some artistic 19 | control over the development of that Package while still keeping the 20 | Package available as open source and free software. 21 | 22 | You are always permitted to make arrangements wholly outside of this 23 | license directly with the Copyright Holder of a given Package. If the 24 | terms of this license do not permit the full use that you propose to 25 | make of the Package, you should contact the Copyright Holder and seek 26 | a different licensing arrangement. 27 | 28 | Definitions 29 | 30 | "Copyright Holder" means the individual(s) or organization(s) 31 | named in the copyright notice for the entire Package. 32 | 33 | "Contributor" means any party that has contributed code or other 34 | material to the Package, in accordance with the Copyright Holder's 35 | procedures. 36 | 37 | "You" and "your" means any person who would like to copy, 38 | distribute, or modify the Package. 39 | 40 | "Package" means the collection of files distributed by the 41 | Copyright Holder, and derivatives of that collection and/or of 42 | those files. A given Package may consist of either the Standard 43 | Version, or a Modified Version. 44 | 45 | "Distribute" means providing a copy of the Package or making it 46 | accessible to anyone else, or in the case of a company or 47 | organization, to others outside of your company or organization. 48 | 49 | "Distributor Fee" means any fee that you charge for Distributing 50 | this Package or providing support for this Package to another 51 | party. It does not mean licensing fees. 52 | 53 | "Standard Version" refers to the Package if it has not been 54 | modified, or has been modified only in ways explicitly requested 55 | by the Copyright Holder. 56 | 57 | "Modified Version" means the Package, if it has been changed, and 58 | such changes were not explicitly requested by the Copyright 59 | Holder. 60 | 61 | "Original License" means this Artistic License as Distributed with 62 | the Standard Version of the Package, in its current version or as 63 | it may be modified by The Perl Foundation in the future. 64 | 65 | "Source" form means the source code, documentation source, and 66 | configuration files for the Package. 67 | 68 | "Compiled" form means the compiled bytecode, object code, binary, 69 | or any other form resulting from mechanical transformation or 70 | translation of the Source form. 71 | 72 | 73 | Permission for Use and Modification Without Distribution 74 | 75 | (1) You are permitted to use the Standard Version and create and use 76 | Modified Versions for any purpose without restriction, provided that 77 | you do not Distribute the Modified Version. 78 | 79 | 80 | Permissions for Redistribution of the Standard Version 81 | 82 | (2) You may Distribute verbatim copies of the Source form of the 83 | Standard Version of this Package in any medium without restriction, 84 | either gratis or for a Distributor Fee, provided that you duplicate 85 | all of the original copyright notices and associated disclaimers. At 86 | your discretion, such verbatim copies may or may not include a 87 | Compiled form of the Package. 88 | 89 | (3) You may apply any bug fixes, portability changes, and other 90 | modifications made available from the Copyright Holder. The resulting 91 | Package will still be considered the Standard Version, and as such 92 | will be subject to the Original License. 93 | 94 | 95 | Distribution of Modified Versions of the Package as Source 96 | 97 | (4) You may Distribute your Modified Version as Source (either gratis 98 | or for a Distributor Fee, and with or without a Compiled form of the 99 | Modified Version) provided that you clearly document how it differs 100 | from the Standard Version, including, but not limited to, documenting 101 | any non-standard features, executables, or modules, and provided that 102 | you do at least ONE of the following: 103 | 104 | (a) make the Modified Version available to the Copyright Holder 105 | of the Standard Version, under the Original License, so that the 106 | Copyright Holder may include your modifications in the Standard 107 | Version. 108 | 109 | (b) ensure that installation of your Modified Version does not 110 | prevent the user installing or running the Standard Version. In 111 | addition, the Modified Version must bear a name that is different 112 | from the name of the Standard Version. 113 | 114 | (c) allow anyone who receives a copy of the Modified Version to 115 | make the Source form of the Modified Version available to others 116 | under 117 | 118 | (i) the Original License or 119 | 120 | (ii) a license that permits the licensee to freely copy, 121 | modify and redistribute the Modified Version using the same 122 | licensing terms that apply to the copy that the licensee 123 | received, and requires that the Source form of the Modified 124 | Version, and of any works derived from it, be made freely 125 | available in that license fees are prohibited but Distributor 126 | Fees are allowed. 127 | 128 | 129 | Distribution of Compiled Forms of the Standard Version 130 | or Modified Versions without the Source 131 | 132 | (5) You may Distribute Compiled forms of the Standard Version without 133 | the Source, provided that you include complete instructions on how to 134 | get the Source of the Standard Version. Such instructions must be 135 | valid at the time of your distribution. If these instructions, at any 136 | time while you are carrying out such distribution, become invalid, you 137 | must provide new instructions on demand or cease further distribution. 138 | If you provide valid instructions or cease distribution within thirty 139 | days after you become aware that the instructions are invalid, then 140 | you do not forfeit any of your rights under this license. 141 | 142 | (6) You may Distribute a Modified Version in Compiled form without 143 | the Source, provided that you comply with Section 4 with respect to 144 | the Source of the Modified Version. 145 | 146 | 147 | Aggregating or Linking the Package 148 | 149 | (7) You may aggregate the Package (either the Standard Version or 150 | Modified Version) with other packages and Distribute the resulting 151 | aggregation provided that you do not charge a licensing fee for the 152 | Package. Distributor Fees are permitted, and licensing fees for other 153 | components in the aggregation are permitted. The terms of this license 154 | apply to the use and Distribution of the Standard or Modified Versions 155 | as included in the aggregation. 156 | 157 | (8) You are permitted to link Modified and Standard Versions with 158 | other works, to embed the Package in a larger work of your own, or to 159 | build stand-alone binary or bytecode versions of applications that 160 | include the Package, and Distribute the result without restriction, 161 | provided the result does not expose a direct interface to the Package. 162 | 163 | 164 | Items That are Not Considered Part of a Modified Version 165 | 166 | (9) Works (including, but not limited to, modules and scripts) that 167 | merely extend or make use of the Package, do not, by themselves, cause 168 | the Package to be a Modified Version. In addition, such works are not 169 | considered parts of the Package itself, and are not subject to the 170 | terms of this license. 171 | 172 | 173 | General Provisions 174 | 175 | (10) Any use, modification, and distribution of the Standard or 176 | Modified Versions is governed by this Artistic License. By using, 177 | modifying or distributing the Package, you accept this license. Do not 178 | use, modify, or distribute the Package, if you do not accept this 179 | license. 180 | 181 | (11) If your Modified Version has been derived from a Modified 182 | Version made by someone other than you, you are nevertheless required 183 | to ensure that your Modified Version complies with the requirements of 184 | this license. 185 | 186 | (12) This license does not grant you the right to use any trademark, 187 | service mark, tradename, or logo of the Copyright Holder. 188 | 189 | (13) This license includes the non-exclusive, worldwide, 190 | free-of-charge patent license to make, have made, use, offer to sell, 191 | sell, import and otherwise transfer the Package with respect to any 192 | patent claims licensable by the Copyright Holder that are necessarily 193 | infringed by the Package. If you institute patent litigation 194 | (including a cross-claim or counterclaim) against any party alleging 195 | that the Package constitutes direct or contributory patent 196 | infringement, then this Artistic License to you shall terminate on the 197 | date that such litigation is filed. 198 | 199 | (14) Disclaimer of Warranty: 200 | THE PACKAGE IS PROVIDED BY THE COPYRIGHT HOLDER AND CONTRIBUTORS "AS 201 | IS' AND WITHOUT ANY EXPRESS OR IMPLIED WARRANTIES. THE IMPLIED 202 | WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR 203 | NON-INFRINGEMENT ARE DISCLAIMED TO THE EXTENT PERMITTED BY YOUR LOCAL 204 | LAW. UNLESS REQUIRED BY LAW, NO COPYRIGHT HOLDER OR CONTRIBUTOR WILL 205 | BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 206 | DAMAGES ARISING IN ANY WAY OUT OF THE USE OF THE PACKAGE, EVEN IF 207 | ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 208 | 209 | 210 | -------- 211 | -------------------------------------------------------------------------------- /test/generic.js: -------------------------------------------------------------------------------- 1 | // Copyright IBM Corp. 2013. All Rights Reserved. 2 | // Node module: strong-mq 3 | // This file is licensed under the Artistic License 2.0. 4 | // License text available at https://opensource.org/licenses/Artistic-2.0 5 | 6 | // generic api tests, should run against all providers 7 | 8 | var assert = require('assert'); 9 | var dbg = require('../lib/dbg'); 10 | var slmq = require('../'); 11 | 12 | 13 | PROVIDERS = [ 14 | 'stomp', 15 | //'amqp', 16 | 'native', 17 | ]; 18 | 19 | 20 | function forEachProvider(callback) { 21 | PROVIDERS.forEach(function(provider) { 22 | describe(provider + ' provider', function () { 23 | describe('with options object', function() { 24 | callback(provider, {provider: provider}); 25 | }); 26 | describe('with options url', function() { 27 | callback(provider, provider + ':'); 28 | }); 29 | }); 30 | }); 31 | } 32 | 33 | 34 | describe('regardless of provider', function() { 35 | describe('create', function() { 36 | it('should create native with no options', function() { 37 | var mq = slmq.create(); 38 | assert.equal(mq.provider, 'native'); 39 | }); 40 | 41 | it('should throw if options has no provider', function() { 42 | assert.throws(function() { 43 | slmq.create({}); 44 | }); 45 | }); 46 | 47 | it('shold throw if options has unknown provider', function() { 48 | assert.throws(function() { 49 | slmq.create({provider: 'no such provider'}); 50 | }); 51 | }); 52 | 53 | it('shold throw if url has unknown provider', function() { 54 | assert.throws(function() { 55 | slmq.create('nosuchprovidereverreallyatall://localhost'); 56 | }); 57 | }); 58 | 59 | }); 60 | }); 61 | 62 | 63 | forEachProvider(function(provider, options) { 64 | it('should have a provider property', function() { 65 | var mq = slmq.create({provider: provider}); 66 | assert.equal(mq.provider, provider); 67 | }); 68 | 69 | 70 | (function() { 71 | function openAndClose(options, done) { 72 | slmq.create(options) 73 | .open() 74 | .close(function() { 75 | // don't call done twice 76 | if (done) { 77 | done(); 78 | }}) 79 | .once('error', function(er) { 80 | if (er.code === 'ECONNRESET') { 81 | // Some rabbitmq servers don't like to have the connection 82 | // just opened and closed. This code path shouldn't effect 83 | // providers that don't have floppy ears. 84 | er = null; 85 | } 86 | done(er); 87 | done = null; 88 | }); 89 | } 90 | 91 | //XXX(sam) I was pretty sure //localhost was required by amqp, what's up? 92 | it('should open and close', function(done) { 93 | openAndClose(options, done); 94 | }); 95 | })(); 96 | 97 | 98 | // XXX(sam) next are difficult, we are victim of underlying lib, I wanted 99 | // them because its nice to detect usage errors immediately, rather than just 100 | // damaging the connection which shows up later. 101 | // XXX(sam) if these were being implemented, I would wrap and paramaterize for 102 | // each provider 103 | describe.skip('when open and close are misused', function() { 104 | it('should throw or ignore multiple open', function(done) { }); 105 | 106 | it('should throw or ignore close when never opened', function() { }); 107 | 108 | it('should throw on close after closed', function(done) { }); 109 | 110 | }); 111 | 112 | 113 | it('should open and close a push queue', function(done) { 114 | var mq = slmq.create(options).open(); 115 | var q = mq.createPushQueue('june'); 116 | assert(q); 117 | assert(q.type === 'push'); 118 | assert(q.name === 'june'); 119 | mq.close(done); 120 | }); 121 | 122 | 123 | it('should open and close a pull queue', function(done) { 124 | var mq = slmq.create(options).open(); 125 | var q = mq.createPullQueue('june'); 126 | assert(q); 127 | assert(q.type === 'pull'); 128 | assert(q.name === 'june'); 129 | mq.close(done); 130 | }); 131 | 132 | 133 | it('should open and double close a push queue', function(done) { 134 | var mq = slmq.create(options).open(); 135 | var q = mq.createPushQueue('june'); 136 | q.close(); 137 | q.close(); 138 | mq.close(done); 139 | }); 140 | 141 | 142 | it('should open and double close a pull queue', function(done) { 143 | var mq = slmq.create(options).open(); 144 | var q = mq.createPullQueue('june'); 145 | q.close(); 146 | q.close(); 147 | mq.close(done); 148 | }); 149 | 150 | 151 | describe('on work queue push then pull', function() { 152 | var cpush, qpush, cpull, qpull; 153 | 154 | beforeEach(function() { 155 | cpush = slmq.create(options).open(); 156 | qpush = cpush.createPushQueue('june'); 157 | cpull = slmq.create(options).open(); 158 | qpull = cpull.createPullQueue('june'); 159 | }); 160 | 161 | afterEach(function(done) { 162 | cpull.close(function() { 163 | cpush.close(done); 164 | }); 165 | }); 166 | 167 | it('should deliver strings', function(done) { 168 | var obj = 'bonjour!'; 169 | qpush.publish(obj); 170 | qpull.subscribe(check); 171 | function check(msg) { 172 | assert.equal(msg, obj); 173 | done(); 174 | } 175 | }); 176 | 177 | it('should deliver buffers', function(done) { 178 | var obj = Buffer('bonjour!'); 179 | qpush.publish(obj); 180 | qpull.subscribe(check); 181 | function check(msg) { 182 | assert.equal(String(msg), obj); 183 | done(); 184 | } 185 | }); 186 | 187 | it('should deliver objects', function(done) { 188 | var obj = {salutation: 'bonjour!'}; 189 | qpush.publish(obj); 190 | qpull.subscribe(check); 191 | function check(msg) { 192 | assert.deepEqual(msg, obj); 193 | done(); 194 | } 195 | }); 196 | 197 | it('should deliver arrays', function(done) { 198 | var obj = {salutation: 'bonjour!'}; 199 | qpush.publish(obj); 200 | qpull.subscribe(check); 201 | function check(msg) { 202 | assert.deepEqual(msg, obj); 203 | done(); 204 | } 205 | }); 206 | }); 207 | 208 | it('should open and double close a pub queue', function(done) { 209 | var mq = slmq.create(options).open(); 210 | var q = mq.createPubQueue('june'); 211 | q.close(); 212 | q.close(); 213 | mq.close(done); 214 | }); 215 | 216 | 217 | it('should open and double close a sub queue', function(done) { 218 | var mq = slmq.create(options).open(); 219 | var q = mq.createSubQueue('june'); 220 | q.close(); 221 | q.close(); 222 | mq.close(done); 223 | }); 224 | 225 | 226 | it('should allow subscribe before publish', function(done) { 227 | var cpub, qpub, csub, qsub; 228 | csub = slmq.create(options).open(); 229 | qsub = csub.createSubQueue('leonie'); 230 | 231 | qsub.subscribe(''); 232 | csub.close(function() { 233 | done(); 234 | }); 235 | }); 236 | 237 | // XXX(sam) test multiple pub and sub on same queue name 238 | 239 | 240 | describe('on topic queue subscribe then publish', function() { 241 | var cpub, qpub, csub, qsub; 242 | var republish; 243 | 244 | beforeEach(function() { 245 | cpub = slmq.create(options).open(); 246 | qpub = cpub.createPubQueue('leonie'); 247 | csub = slmq.create(options).open(); 248 | qsub = csub.createSubQueue('leonie'); 249 | republish = true; 250 | }); 251 | 252 | afterEach(function(done) { 253 | republish = false; 254 | csub.close(function() { 255 | cpub.close(done); 256 | }); 257 | }); 258 | 259 | function shouldMatchTopic(pubTopic, subTopic) { 260 | function printable(s) { 261 | if (s == null) { 262 | return s; 263 | } 264 | return '"'+s+'"'; 265 | } 266 | it('should subscribe on ' + printable(subTopic) + ' and ' + 267 | 'receive topic ' + printable(pubTopic), function(done) { 268 | var obj = 'quelle affaire'; 269 | 270 | qsub.subscribe(subTopic, function(msg) { 271 | if(done) { 272 | assert.equal(obj, msg); 273 | done(); 274 | done = null; 275 | } 276 | }); 277 | 278 | // Race condition, publications are dropped until broker knows about 279 | // subscription, by design, but we don't know when that has happened. 280 | // Work-around is to keep publishing until test is done. 281 | setImmediate(republishLoop); 282 | function republishLoop() { 283 | if (republish) { 284 | qpub.publish(obj, pubTopic); 285 | setTimeout(republishLoop, 50); 286 | } 287 | } 288 | }); 289 | } 290 | 291 | it('should close unsubscribed queues', function(done) { 292 | var cpub, qpub, csub, qsub; 293 | cpub = slmq.create(options).open(); 294 | qpub = cpub.createPubQueue('leonie'); 295 | csub = slmq.create(options).open(); 296 | qsub = csub.createSubQueue('leonie'); 297 | cpub.close(function() { 298 | csub.close(function() { 299 | done(); 300 | }); 301 | }); 302 | }); 303 | 304 | shouldMatchTopic('some.thing.specific.deep', 'some.thing'); 305 | shouldMatchTopic('some.thing.specific', 'some.thing'); 306 | shouldMatchTopic('some.thing', 'some.thing'); 307 | 308 | shouldMatchTopic('some.thing.specific', 'some'); 309 | shouldMatchTopic('some.thing', 'some'); 310 | shouldMatchTopic('some', 'some'); 311 | 312 | shouldMatchTopic('some.thing.specific', ''); 313 | shouldMatchTopic('some.thing', ''); 314 | shouldMatchTopic('some', ''); 315 | shouldMatchTopic('', ''); 316 | shouldMatchTopic(null, ''); 317 | 318 | shouldMatchTopic('some.thing.specific', null); 319 | shouldMatchTopic('some.thing', null); 320 | shouldMatchTopic('some', null); 321 | shouldMatchTopic('', null); 322 | shouldMatchTopic(null, null); 323 | }); 324 | }); 325 | --------------------------------------------------------------------------------