├── examples ├── coffee │ ├── server.coffee │ ├── config.coffee │ └── queue.coffee ├── basic │ ├── app.js │ ├── talker.js │ └── listener.js ├── hello │ ├── app.js │ ├── greeter.js │ └── talker.js └── common │ └── child.js ├── .gitignore ├── lib ├── lib.js ├── errors.js ├── eventer.js ├── logger.js ├── QueueLogger.js ├── options.js ├── QueueMonitor.js ├── MongoMQ.js └── MongoConnection.js ├── .project ├── .gitattributes ├── package.json ├── license.txt ├── bin └── MongoMQ.js └── readme.md /examples/coffee/server.coffee: -------------------------------------------------------------------------------- 1 | require './queue' -------------------------------------------------------------------------------- /examples/coffee/config.coffee: -------------------------------------------------------------------------------- 1 | exports.db = 2 | host: 'localhost' 3 | name: 'tests' 4 | 5 | exports.db.url = "mongodb://#{exports.db.host}/#{exports.db.name}" -------------------------------------------------------------------------------- /examples/basic/app.js: -------------------------------------------------------------------------------- 1 | var Children = require('../common/child'); 2 | var talker = Children.startChild('./talker'); 3 | var listener = Children.startChild('./listener'); 4 | -------------------------------------------------------------------------------- /examples/hello/app.js: -------------------------------------------------------------------------------- 1 | var Children = require('../common/child'); 2 | var talker = Children.startChild('./talker'); 3 | var listener = Children.startChild('./greeter'); 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ################# 2 | ## npm/Node.js 3 | ################# 4 | 5 | node_modules 6 | 7 | ############ 8 | ## Windows 9 | ############ 10 | 11 | # Windows image file caches 12 | Thumbs.db 13 | 14 | # Folder config file 15 | Desktop.ini 16 | 17 | #Editor metadata 18 | .idea -------------------------------------------------------------------------------- /lib/lib.js: -------------------------------------------------------------------------------- 1 | exports.MongoMQ = require('./MongoMQ'); 2 | exports.Eventer = require('./eventer'); 3 | exports.Logger = require('./logger'); 4 | exports.MongoConnection = require('./MongoConnection'); 5 | exports.options = require('./options'); 6 | exports.QueueMonitor = require('./QueueMonitor'); 7 | -------------------------------------------------------------------------------- /.project: -------------------------------------------------------------------------------- 1 | 2 | 3 | MongoMQ 4 | 5 | 6 | 7 | 8 | 9 | 10 | com.aptana.projects.webnature 11 | 12 | 13 | -------------------------------------------------------------------------------- /examples/coffee/queue.coffee: -------------------------------------------------------------------------------- 1 | config = require './config' 2 | MongoMQ = (require '../../lib/lib').MongoMQ 3 | 4 | queue = new MongoMQ 5 | autoStart: true 6 | host: config.db.host 7 | collectionName: 'capped_collection' 8 | database: config.db.name 9 | 10 | #queue.start() 11 | 12 | exports.queue = queue -------------------------------------------------------------------------------- /lib/errors.js: -------------------------------------------------------------------------------- 1 | var errors = module.exports = { 2 | E_CONNCLOSED: 'No active server connection!', 3 | E_NODB: 'No database name provided!', 4 | E_INVALIDFILTER: 'Invalid or no filter provided!', 5 | E_NOCALLBACK: 'No callback provided, callback is required!', 6 | E_INVALIDINDEX: 'Invalid or no index provided!', 7 | E_INVALIDCURSORCOLLECTION: 'Supplied collection is not capped, tailed cursors only work on capped collections!' 8 | }; -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | 4 | # Custom for Visual Studio 5 | *.cs diff=csharp 6 | *.sln merge=union 7 | *.csproj merge=union 8 | *.vbproj merge=union 9 | *.fsproj merge=union 10 | *.dbproj merge=union 11 | 12 | # Standard to msysgit 13 | *.doc diff=astextplain 14 | *.DOC diff=astextplain 15 | *.docx diff=astextplain 16 | *.DOCX diff=astextplain 17 | *.dot diff=astextplain 18 | *.DOT diff=astextplain 19 | *.pdf diff=astextplain 20 | *.PDF diff=astextplain 21 | *.rtf diff=astextplain 22 | *.RTF diff=astextplain 23 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": { 3 | "name": "Jeremy Darling", 4 | "email": "jeremy.darling@gmail.com" 5 | }, 6 | "name": "mongomq", 7 | "version": "0.3.9", 8 | "repository": { 9 | "type": "git", 10 | "url": "git://github.com/jdarling/MongoMQ.git" 11 | }, 12 | "main": "./lib/lib", 13 | "license": "MIT", 14 | "contributors": [ 15 | { 16 | "name": "mfrobben" 17 | }, 18 | { 19 | "name": "joscha" 20 | }, 21 | { 22 | "name": "nickpalmer" 23 | } 24 | ], 25 | "engines": { 26 | "node": ">= v0.8.2" 27 | }, 28 | "dependencies": { 29 | "mongodb": "^2.2.16", 30 | "node-uuid": "^1.4.7" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /examples/basic/talker.js: -------------------------------------------------------------------------------- 1 | var MC = require('../../lib/lib').MongoConnection; 2 | var MQ = require('../../lib/lib').MongoMQ; 3 | 4 | var options = {databaseName: 'tests', queueCollection: 'capped_collection', autoStart: false}; 5 | //var options = {servers: ['ndcsrvcdep601', 'ndcsrvcdep602'], databaseName: 'tests', queueCollection: 'capped_collection', autoStart: false}; 6 | 7 | var mq = module.exports = new MQ(options); 8 | 9 | var recordNumber = 0; 10 | var putRecord = function(){ 11 | console.log('Emitting: '+recordNumber); 12 | mq.emit('test', recordNumber); 13 | recordNumber++; 14 | setTimeout(putRecord, 5); 15 | }; 16 | 17 | (function(){ 18 | var logger = new MC(options); 19 | logger.open(function(err, mc){ 20 | if(err){ 21 | console.log('ERROR: ', err); 22 | }else{ 23 | mc.collection('log', function(err, loggingCollection){ 24 | loggingCollection.remove({}, function(){ 25 | mq.start(function(err){ 26 | putRecord(); 27 | }); 28 | }); 29 | }); 30 | } 31 | }); 32 | })(); 33 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /examples/hello/greeter.js: -------------------------------------------------------------------------------- 1 | var MC = require('../../lib/lib').MongoConnection; 2 | var MQ = require('../../lib/lib').MongoMQ; 3 | 4 | var options = {host: 'localhost', databaseName: 'tests', queueCollection: 'capped_collection', autoStart: true, 5 | serverOptions: { 6 | socketOptions: { 7 | connectTimeoutMS: 15000, 8 | socketTimeoutMS: 15000 9 | } 10 | } 11 | }; 12 | 13 | var mq = module.exports = new MQ(options); 14 | 15 | var log; 16 | 17 | var handleRecord = function(err, data, next){ 18 | if(!err){ 19 | log.insert({handled: data}, {w:0}); 20 | next('Hello '+(data||'world')+'!'); 21 | }else{ 22 | console.log('err: ', err); 23 | next(); 24 | } 25 | }; 26 | 27 | mq.on('greet', handleRecord); 28 | 29 | (function(){ 30 | var logger = new MC(options); 31 | logger.open(function(err, mc){ 32 | if(err){ 33 | console.log('ERROR: ', err); 34 | }else{ 35 | mc.collection('log', function(err, loggingCollection){ 36 | log = loggingCollection; 37 | if(!options.autoStart){ 38 | mq.start(function(err){ 39 | if(err){ 40 | console.log(err); 41 | } 42 | }); 43 | } 44 | }); 45 | } 46 | }); 47 | })(); 48 | -------------------------------------------------------------------------------- /examples/hello/talker.js: -------------------------------------------------------------------------------- 1 | var MC = require('../../lib/lib').MongoConnection; 2 | var MQ = require('../../lib/lib').MongoMQ; 3 | 4 | var options = {host: 'localhost', databaseName: 'tests', queueCollection: 'capped_collection', autoStart: true}; 5 | 6 | var mq = module.exports = new MQ(options); 7 | 8 | var recordNumber = 0; 9 | var emitRecord = function(){ 10 | console.log('emitting '+recordNumber); 11 | mq.emit('greet', recordNumber, function(err, data){ 12 | console.log('Response:'); 13 | console.log(' err>', err); 14 | console.log(' dat>', data); 15 | emitRecord(); 16 | }); 17 | recordNumber++; 18 | }; 19 | 20 | mq.ready(function(){ 21 | console.log('ready'); 22 | }); 23 | 24 | var putRecord = function(){ 25 | setTimeout(function(){ 26 | emitRecord(); 27 | putRecord(); 28 | }, 100); 29 | }; 30 | 31 | (function(){ 32 | var logger = new MC(options); 33 | logger.open(function(err, mc){ 34 | if(err){ 35 | console.log('ERROR: ', err); 36 | }else{ 37 | mc.collection('log', function(err, loggingCollection){ 38 | loggingCollection.remove({}, function(){ 39 | if(!options.autoStart){ 40 | /* 41 | mq.start(function(err){ 42 | putRecord(); 43 | }); 44 | */ 45 | } 46 | }); 47 | }); 48 | } 49 | }); 50 | })(); 51 | 52 | emitRecord(); 53 | -------------------------------------------------------------------------------- /examples/common/child.js: -------------------------------------------------------------------------------- 1 | var spawn = require('child_process').spawn; 2 | 3 | var _children = []; 4 | 5 | var Children = function(){ 6 | }; 7 | 8 | Children.prototype.startChild = function(fileName){ 9 | var args = Array.prototype.slice.call(arguments); 10 | var child = spawn('node', args); 11 | child.processFile = fileName; 12 | child.arguments = args; 13 | child.on('exit', (function(aChild){ 14 | return function(code, signal){ 15 | var idx = _children.indexOf(aChild); 16 | if(idx>-1&&_children[idx]) console.log(('Child ('+_children[idx].processFile+') '+aChild.pid+' is dying!').yellow); 17 | else console.log(('Child ('+aChild.pid+') is dying!').yellow); 18 | if(idx>-1){ 19 | _children.splice(idx, 1); 20 | console.log('Child killed successfully!'.green); 21 | } 22 | }; 23 | })(child)); 24 | child.on('uncaughtException', (function(aChild){ 25 | return function(err){ 26 | console.error('EXCEPTION: ', err); 27 | }; 28 | })(child)); 29 | child.stdout.on('data', (function(aChild){ 30 | return function(data){ 31 | console.log(data.toString()); 32 | }; 33 | })(child)); 34 | child.stderr.on('data', (function(aChild){ 35 | return function(data){ 36 | console.error('ERROR: ', data.toString()); 37 | }; 38 | })(child)); 39 | _children.push(child); 40 | return child; 41 | }; 42 | 43 | Children.prototype.children = function(){ 44 | return _children; 45 | }; 46 | 47 | module.exports = new Children(); 48 | -------------------------------------------------------------------------------- /examples/basic/listener.js: -------------------------------------------------------------------------------- 1 | var MC = require('../../lib/lib').MongoConnection; 2 | var MQ = require('../../lib/lib').MongoMQ; 3 | 4 | var options = {databaseName: 'tests', queueCollection: 'capped_collection', autoStart: false}; 5 | //var options = {servers: ['ndcsrvcdep601', 'ndcsrvcdep602'], databaseName: 'tests', queueCollection: 'capped_collection', autoStart: false}; 6 | 7 | //options.listenerType = 'streams'; 8 | //options.listenerType = 'nextObject'; 9 | 10 | //Streams are great for broadcast event listeners, they are BAD for things that require processing and response 11 | // as they can allow for node saturation and they are greedy. The default listenerType is 'nextObject', you 12 | // can also set the listener type on the listener itself using: 13 | // MQ.on('event', {listenerType: ''}, callback) or 14 | // MQ.once('event', {listenerType: ''}, callback) 15 | 16 | var mq = module.exports = new MQ(options); 17 | 18 | var log; 19 | 20 | var handleRecord = function(err, data, next){ 21 | var w = Math.floor(Math.random()*100); 22 | if(!err){ 23 | console.log('data: ', data, 'wait: ', w); 24 | log.insert({handled: data}, {w:0}); 25 | }else{ 26 | console.log('err: ', err, 'wait: ', w); 27 | } 28 | next(); 29 | }; 30 | 31 | mq.on('test', handleRecord); 32 | 33 | (function(){ 34 | var logger = new MC(options); 35 | logger.open(function(err, mc){ 36 | if(err){ 37 | console.log('ERROR: ', err); 38 | }else{ 39 | mc.collection('log', function(err, loggingCollection){ 40 | log = loggingCollection; 41 | mq.start(function(err){ 42 | if(err){ 43 | console.log(err); 44 | } 45 | }); 46 | }); 47 | } 48 | }); 49 | })(); 50 | -------------------------------------------------------------------------------- /lib/eventer.js: -------------------------------------------------------------------------------- 1 | var util = require('util'); 2 | var EventEmitter = require('events').EventEmitter; 3 | 4 | var defaultError = function () {}; 5 | 6 | var Eventer = module.exports = function(hosting){ 7 | var self = this; 8 | self.hosting = hosting; 9 | self.on('error', defaultError); 10 | }; 11 | 12 | util.inherits(Eventer, EventEmitter); 13 | 14 | Eventer.description = 'A wrapper around EventEmitter to provide some high level functionality and eased useage pattern within MongoMQ.'; 15 | 16 | Eventer.prototype.call = function(eventName, err, response){ 17 | var self = this; 18 | self.emit(eventName, err, response||self.hosting); 19 | }; 20 | Eventer.prototype.call.description = 'Calls an event.'; 21 | 22 | Eventer.prototype.registerTo = function(what){ 23 | var self = this, keys = Object.keys(self), i, l=keys.length, key, value; 24 | for(i=0; i0){ 47 | self.emit('error', err, self.hosting); 48 | } 49 | if(hasCallback){ 50 | callback(err); 51 | } 52 | if((handlerCount===0)&&(!noThrow)&&(!hasCallback)){ 53 | throw err.toString(); 54 | } 55 | return false; 56 | }; 57 | Eventer.prototype.checkNoError.description = 'Checks to see if there is an error, and if their is handles it appropriatly.'; 58 | 59 | Eventer.prototype.surface = function(what){ 60 | what = what instanceof Array?what:[what]; 61 | var self = this, i, l = what.length, event; 62 | for(i=0; i1){ 18 | name = names.shift(); 19 | name = alias[name]||name; 20 | tmp = tmp[name]=tmp[name]||{}; 21 | } 22 | name = names.shift(); 23 | name = alias[name]||name; 24 | tmp[name]=val; 25 | } 26 | 27 | var queue = new MongoMQ(opts); 28 | 29 | queue.ready(function(){ 30 | console.log('Connected'); 31 | }); 32 | 33 | queue.stopped(function(){ 34 | console.log('Disconnected'); 35 | }); 36 | 37 | queue.error(function(err){ 38 | console.log(err); 39 | }); 40 | 41 | var r = repl.start({ 42 | prompt: "MongoMQ>" 43 | }); 44 | 45 | r.on('exit', function(){ 46 | queue.stop(); // force a close 47 | }); 48 | 49 | var funcName, value, funcs = [], ftmp; 50 | for(funcName in queue){ 51 | value = queue[funcName]; 52 | if(typeof(value)=='function'){ 53 | ftmp = r.context[funcName]||(function(f){ 54 | return function(){ 55 | f.apply(queue, arguments); 56 | }; 57 | })(value); 58 | funcs.push({name: funcName, description: value.description, f: ftmp, a: value}); 59 | if(!r.context[funcName]){ 60 | r.context[funcName] = ftmp; 61 | } 62 | } 63 | } 64 | 65 | r.context.connect = function(connectionString, databaseName, collectionName){ 66 | queue.close(function(){ 67 | queue.options.connectionString = connectionString||queue.options.connectionString; 68 | queue.options.databaseName = databaseName||queue.options.databaseName; 69 | queue.options.queueCollection = collectionName||queue.options.queueCollection; 70 | queue.start(function(err){ 71 | if(err){ 72 | console.log(err); 73 | } 74 | }); 75 | }); 76 | }; 77 | r.context.connect.description = 'Connect to a specific MongoMQ instance to work with'; 78 | 79 | r.context.status = function(callback){ 80 | callback = callback || function(err, results, info){ 81 | results = results || []; 82 | var i, l=results.length; 83 | console.log('\r\n-=[Queue Status]=-'); 84 | for(i=0; i '+funcs[i].description+'\r\n'; 102 | } 103 | return result; 104 | }; 105 | if(methodName){ 106 | var i = false, l = funcs.length, index = false; 107 | for(i = 0; (i" 145 | }); 146 | r.on('exit', function(){ 147 | queue.stop(); 148 | }); 149 | 150 | var msgidx = 0; 151 | r.context.send = function(){ 152 | queue.emit('test', msgidx); 153 | msgidx++; 154 | }; 155 | 156 | r.context.load = function(){ 157 | for(var i = 0; i<100; i++){ 158 | queue.emit('test', msgidx); 159 | msgidx++; 160 | } 161 | }; 162 | 163 | var logMsg = function(err, data, next){ 164 | console.log('LOG: ', data); 165 | next(); 166 | }; 167 | var eatTest = function(err, data, next){ 168 | console.log('eat: ', data); 169 | next(); 170 | }; 171 | 172 | r.context.logAny = function(){ 173 | queue.onAny(logMsg); 174 | }; 175 | 176 | r.context.listen = function(){ 177 | queue.on('test', eatTest); 178 | }; 179 | 180 | r.context.start = function(cb){ 181 | queue.start(cb); 182 | }; 183 | 184 | r.context.stop = function(){ 185 | queue.stop(); 186 | }; 187 | 188 | r.context.help = function(){ 189 | console.log('Built in test methods:\r\n'+ 190 | ' help() - shows this message\r\n'+ 191 | ' logAny() - logs any message to the console\r\n'+ 192 | ' eatTest() - consumes next available "test" message from the queue\r\n'+ 193 | ' send() - places a "test" message on the queue\r\n'+ 194 | ' load() - places 100 "test" messages on the queue\r\n'+ 195 | ' start() - start the queue listener\r\n'+ 196 | ' stop() - stop the queue listener\r\n'+ 197 | '\r\nInstance Data\r\n'+ 198 | ' queue - the global MongoMQ instance\r\n' 199 | ); 200 | return ''; 201 | }; 202 | 203 | /* 204 | queue.start(function(){ 205 | r.context.eatTest(); 206 | }); 207 | */ 208 | 209 | r.context.queue = queue; 210 | 211 | r.context.help(); 212 | ``` 213 | 214 | How Events are stored 215 | ===================== 216 | 217 | ```javascript 218 | { 219 | _id: ObjectId(), // for internal use only 220 | pkt_ver: 3, // Packet version that this message is being sent in 221 | event: event, // string that represents what type of event this is 222 | data: message, // Contains the actual message contents 223 | handled: false, // states if the message has been handled or not 224 | localTime: dt, // Local Date Time of when the message was put on the queue 225 | globalTime: new Date(dt-self.serverTimeOffset), // Date Time offset to server time of when the message was put on the queue 226 | pickedTime: new Date(dt-self.serverTimeOffset), // Date Time offset to server time of when the message was picked up from the queue 227 | host: string, // Contains the host name of the machine that initiated the event 228 | [response_id: string] // optional if the event expects response(s) this will be the conversation identifier used to track those responses 229 | } 230 | ``` 231 | 232 | Update History 233 | ============== 234 | 235 | v0.3 Update History 236 | ------------------- 237 | 238 | v0.3.4 239 | * Fix QueueMonitor.js call from options to self.options in handleResponse 240 | 241 | v0.3.3 242 | * Fixed package.json and upreved version to pickup changes. 243 | 244 | v0.3.2 245 | * Upgraded to latest (1.3.6) version of Mongo Node Native 246 | * Fixed typo in lib.js for inclusion of Logging (changed Logging to logging) 247 | * Locked dependency versions so breaking shouldn't happen again when the dependency chain changes 248 | 249 | v0.3.1 250 | * Added setTimeout to nextTick on startup to give Mongo a chance to get connceted to 251 | * Minor bug fix due to EventEmitter treating 'error' events specially 252 | * Tweak to once listeners to call next if it exists. Shouldn't change anything but it is good practice. 253 | 254 | v0.3.0 255 | * Initial release of v0.3.x, includes many new features and functionality along with many bug fixes. 256 | 257 | v0.2 Update History 258 | ------------------- 259 | 260 | v0.2.10&v0.2.11 261 | * Workaround for Mongo Native Driver not supporting tailed cursor auto-reconnects when Mongo server goes away. 262 | 263 | v0.2.9 264 | * Change SafeDBDriver default value from false to true, this fixes the issue with multiple listeners picking up the same message since Mongo doesn't perform record locking on updates if this isn't true. 265 | * Fix autoStart 266 | * Resolves #9 and #10 267 | 268 | v0.2.8 269 | * Upgraded code for new MongoDB Native Drivers (thanks mfrobben for starting points) 270 | * Readme cleanup (thanks ttezel for pointing this out and fixing it) 271 | * Resolves #7 and #6 272 | 273 | v0.2.7 274 | * Fixed a cursor leak when using passive callbacks 275 | 276 | v0.2.6 277 | * Bug fix related to relplica set configuration loading from config.json files 278 | 279 | v0.2.5 280 | * General code cleanup and optimizations 281 | * Examples cleanup and fixes 282 | 283 | v0.2.4 284 | * Examples added 285 | 286 | v0.2.3 287 | * Minor bug fix related to passive listeners where a fromDT was not passed in the options 288 | * Added hostName to messages for better tracking/logging 289 | * Modified passive callback to pass the actual message as the "this" argument, you can now use this.event to get the actual event that was responded to 290 | * Updated the on() method to accept strings or regular expressions to filter events on 291 | 292 | v0.2.2 293 | * Completed code to allow for callbacks and partial callbacks to be issued back to emit statements 294 | * Complteed refactoring of code to properly seperate functionality into objects 295 | 296 | v0.2.1 297 | * Majorly refactored code 298 | * Added autoIndexId: true to queue collection creation 299 | * Better MongoMQ application with help() 300 | * Updated test application 301 | * Added an exception to emit() when you try to emit before start() has been called 302 | * fix to onAny so it will restart listeners after a close() and start() re-issue 303 | * Added remove*() methods 304 | * Changed close() to stop() 305 | * hereOnOut options - allows listeners to only pay attention to messages posted after they have been started up 306 | * Added ability to register listeners (via on and onAny) when queue is not started 307 | 308 | v0.1.1 309 | * Bug fixes to on event 310 | * Added in new onAny register 311 | * Migrated code to retain cursor 312 | 313 | v0.1.0 314 | * Initial release 315 | * More of a proof of concept -------------------------------------------------------------------------------- /lib/MongoMQ.js: -------------------------------------------------------------------------------- 1 | var MC = require('./MongoConnection'); 2 | var UUID = require('node-uuid'); 3 | var util = require('util'); 4 | var Options = require('./options'); 5 | var hostName = require('os').hostname(); 6 | var QueueMonitor = require('./QueueMonitor'); 7 | var errors = require('./errors'); 8 | 9 | var defaults = { 10 | autoStart: true, 11 | queueCollection: 'queue', 12 | databaseName: 'mongomq' 13 | }; 14 | 15 | var MongoMQ = module.exports = function(options, callback){ 16 | var self = this, mcOptions; 17 | if(typeof(options)==='function'){ 18 | callback = options; 19 | options = {}; 20 | } 21 | callback=callback||function(){}; 22 | options = options||{}; 23 | options.databaseName=options.databaseName||options.database; 24 | options.queueCollection=options.queueCollection||options.collectionName; 25 | options = Options.ensure(options, defaults); 26 | 27 | mcOptions = Options.ensure(mcOptions, options); 28 | mcOptions.autoStart = false; 29 | 30 | MC.call(self, options); 31 | 32 | self.monitors={}; 33 | self.emitter.surface(['ready', 'stopped']); 34 | 35 | if(options.autoStart){ 36 | self.start(callback); 37 | }else{ 38 | callback(null, self); 39 | } 40 | }; 41 | 42 | util.inherits(MongoMQ, MC); 43 | 44 | MongoMQ.options = defaults; 45 | 46 | MongoMQ.prototype.checkConnection = function(callback){ 47 | var self = this; 48 | var waitForStarted = function(){ 49 | process.nextTick(function(){ 50 | setTimeout(function(){ 51 | if(self.isopen){ 52 | callback(null, self); 53 | }else{ 54 | waitForStarted(); 55 | } 56 | }, 100); 57 | }); 58 | }; 59 | if(self.isopen){ 60 | callback(null, self); 61 | }else if(self.options.autoStart){ 62 | waitForStarted(); 63 | }else{ 64 | callback(new Error(errors.E_CONNCLOSED)); 65 | } 66 | }; 67 | MongoMQ.prototype.checkConnection.description = 'Checks to see if MongoMQ is connected to a server or not.'; 68 | 69 | MongoMQ.prototype.start = function(callback){ 70 | var self = this; 71 | if(self.isopen){ 72 | (callback||function(){})(null, self); 73 | }else{ 74 | self.open(function(err){ 75 | if(err){ 76 | self.close(); 77 | self._open = false; 78 | (callback||function(){})(err); 79 | self.emitter.call('error', err); 80 | }else{ 81 | self.ensureCapped(self.options.queueCollection, function(err, collection){ 82 | if(!self.emitter.checkNoError(err, callback)){ 83 | self.close(); 84 | self._open = false; 85 | }else{ 86 | self.startListeners(function(){ 87 | var startTime = new Date(); 88 | self.serverStatus(function(err, status){ 89 | if(self.emitter.checkNoError(err, callback)){ 90 | self.serverTimeOffset = status.localTime - startTime; 91 | (callback||function(){})(err, self); 92 | self.emitter.call('ready', err); 93 | } 94 | }); 95 | }); 96 | } 97 | }); 98 | } 99 | }); 100 | } 101 | }; 102 | MongoMQ.prototype.start.description = 'Starts the Mongo Queue system.'; 103 | 104 | MongoMQ.prototype.stop = function(callback){ 105 | var self = this; 106 | if(self.isopen){ 107 | self.stopListeners(function(){ 108 | self.close(function(){ 109 | self.emitter.call('stopped'); 110 | self._open = false; 111 | (callback||function(){})(null, self); 112 | }); 113 | }); 114 | }else{ 115 | (callback||function(){})(null, self); 116 | self.emitter.call('stopped'); 117 | } 118 | }; 119 | MongoMQ.prototype.start.description = 'Stops the Mongo Queue system.'; 120 | 121 | MongoMQ.prototype.emit = function(event, message, callback){ 122 | var self = this, hasCallback = typeof(callback)==='function'; 123 | self.checkConnection(function(err){ 124 | if(err){ 125 | self.emitter.call('error', err); 126 | if(typeof(callback)==='function'){ 127 | return callback(err); 128 | }else{ 129 | throw err; 130 | } 131 | } 132 | self.collection(self.options.queueCollection, function(err, collection){ 133 | var dt = new Date(), 134 | pkt = { 135 | pkt_ver: 3, 136 | event: event, 137 | data: message, 138 | handled: false, 139 | localTime: dt, 140 | globalTime: new Date(dt-self.serverTimeOffset), 141 | pickedTime: new Date(dt-self.serverTimeOffset), 142 | host: hostName 143 | }; 144 | if(hasCallback){ 145 | pkt.response_id = UUID.v4(); // new way 146 | if(self.options.support_v2){ 147 | pkt.conversationId = UUID.v4(); // old way 148 | } 149 | } 150 | collection.insert(pkt, {w: 1}, function(err, details){ 151 | self.emitter.call('sent', details); 152 | if(!err){ 153 | if(hasCallback){ 154 | self.once(pkt.response_id, {listenerType: 'responseListener'}, callback); 155 | } 156 | }else{ 157 | //err = err instanceof Error?err:new Error(err); 158 | self.emitter.call('error', err); 159 | if(hasCallback){ 160 | callback(err); 161 | }else{ 162 | throw err; 163 | } 164 | } 165 | }); 166 | }); 167 | }); 168 | }; 169 | MongoMQ.prototype.emit.description = 'Puts a message on the queue.'; 170 | 171 | MongoMQ.prototype.broadcast = function(event, message){ 172 | var self = this; 173 | self.checkConnection(function(err){ 174 | if(err){ 175 | self.emitter.call('error', err); 176 | throw err; 177 | } 178 | self.collection(self.options.queueCollection, function(err, collection){ 179 | var pkt = { 180 | event: event, 181 | data: message, 182 | localTime: new Date(), 183 | host: hostName 184 | }; 185 | collection.insert(pkt, {w: 0}); 186 | }); 187 | }); 188 | }; 189 | MongoMQ.prototype.broadcast.description = 'Broadcasts a message out across all subscribed listeners.'; 190 | 191 | MongoMQ.prototype.status = function(callback){ 192 | var self = this; 193 | if(self.isopen){ 194 | self.checkConnection(function(err){ 195 | if(err){ 196 | self.emitter.call('error', err); 197 | if(typeof(callback)==='function'){ 198 | return callback(err); 199 | }else{ 200 | throw err; 201 | } 202 | } 203 | var map = function(){ 204 | emit(this.event, 1); 205 | }; 206 | var reduce = function(key, values){ 207 | var reduced = 0; 208 | values.forEach(function(val){ 209 | reduced += val; 210 | }); 211 | return reduced; 212 | }; 213 | self.collection(self.options.queueCollection, function(err, collection){ 214 | if(err){ 215 | self.emitter.call('error', err); 216 | callback(err); 217 | }else{ 218 | collection.mapReduce(map, reduce, { 219 | query : { "handled" : false }, 220 | out : { inline : 1 } 221 | }, callback); 222 | } 223 | }); 224 | }); 225 | }else{ 226 | callback(null, false); 227 | } 228 | }; 229 | MongoMQ.prototype.status.description = 'Retrieves the MongoMQ queue stati and their depth.'; 230 | 231 | MongoMQ.prototype.startListeners = function(callback){ 232 | var self = this, eventNames = Object.keys(self.monitors), i, l=eventNames.length, detailItems, j, k; 233 | for(i=0; i-1; i--){ 349 | listener = list[i].stop(); 350 | } 351 | self.monitors[event]=[]; 352 | }; 353 | MongoMQ.prototype.removeAllListeners.description = 'Stops and removes all listeners for a specific event.'; 354 | 355 | MongoMQ.prototype.setMaxListeners = false; 356 | 357 | MongoMQ.prototype.listeners = function(event){ 358 | var self = this; 359 | var list = self.monitors[event]=self.monitors[event]||[]; 360 | return list; 361 | }; 362 | MongoMQ.prototype.listeners.description = 'Returns a listing of listeners and their options for a given event.'; 363 | -------------------------------------------------------------------------------- /lib/MongoConnection.js: -------------------------------------------------------------------------------- 1 | var Mongo = require('mongodb'); 2 | var Mongo = require('mongodb'); 3 | var MongoClient = Mongo.MongoClient; 4 | var Options = require('./options'); 5 | var GridStore = Mongo.GridStore; 6 | var Eventer = require('./eventer'); 7 | var errors = require('./errors'); 8 | 9 | var defaults = { 10 | CollectionOptions: {safe: false}, 11 | ServerHost: 'localhost', 12 | ServerOptions: { 13 | auto_reconnect: false, 14 | //* 15 | poolSize: 1, 16 | socketOptions: { 17 | connectTimeoutMS: 1000, 18 | socketTimeoutMS: 1000 19 | } 20 | //*/ 21 | }, 22 | CappedCollectionSize: 104857600, 23 | NativeParser : false, 24 | logger: null // No logger by default. If providied, this needs to have the same interface as the MongoDB logger. 25 | //logger: new require('./logger')() 26 | }; 27 | 28 | var ensureMongoConnectionDetails = function(options){ 29 | if(options.connectionString){ 30 | return; 31 | }else if(options.servers||options.host){ 32 | Options.ensure(options, {port: 27017}); 33 | return; 34 | }else{ 35 | options.connectionString = 'mongodb://localhost:27017'; 36 | } 37 | }; 38 | 39 | var MongoConnection = module.exports = function(options, callback){ 40 | var self = this, dbName; 41 | callback=callback||function(){}; 42 | options = Options.ensure(options||{}, {autoStart: true}); 43 | ensureMongoConnectionDetails(options); 44 | self.__defineGetter__('options', function(){ 45 | return options; 46 | }); 47 | self.__defineGetter__('active', function(){ 48 | return (!!this.db)&&(this.db.openCalled); 49 | }); 50 | self.__defineGetter__('isopen', function(){ 51 | return this._open; 52 | }); 53 | self.__defineGetter__('databaseName', function(){ 54 | return (this.db||{}).databaseName||'default'; 55 | }); 56 | self.__defineSetter__('databaseName', function(value){ 57 | this.use(value); 58 | }); 59 | self.emitter = new Eventer(self); 60 | self.emitter.surface(['error', 'opened', 'closed']); 61 | self.emitter.registerTo(self); 62 | }; 63 | 64 | MongoConnection.ERROR_CODES = errors; 65 | 66 | var connect = function(self, connection, callback){ 67 | var connected = function(err, database){ 68 | if(self.emitter.checkNoError(err, callback)){ 69 | self.db = database; 70 | self._open = true; 71 | if(self.options.username&&self.options.password){ 72 | if(self.options.authenticateAgainstDb){ 73 | database.authenticate(self.options.username, self.options.password, function(err, result){ 74 | if(typeof(callback)=='function'){ 75 | self.emitter.call('opened', err); 76 | callback(null, self); 77 | } 78 | }); 79 | }else{ 80 | database.admin(function(err, adminDb){ 81 | if(self.emitter.checkNoError(err, callback)){ 82 | adminDb.authenticate(self.options.username, self.options.password, function(err, result){ 83 | if(self.emitter.checkNoError(err, callback)){ 84 | self.emitter.call('opened', err); 85 | callback(null, self); 86 | } 87 | }); 88 | } 89 | }); 90 | } 91 | }else{ 92 | self.emitter.call('opened', err); 93 | callback(null, self); 94 | } 95 | } 96 | }; 97 | if(!connection){ 98 | var connectOptions = {server: Options.ensure(self.options.serverOptions||{}, defaults.ServerOptions)}; 99 | if(self.options.logger||defaults.logger){ 100 | connectOptions.server.logger=self.options.logger||defaults.logger; 101 | connectOptions.server.logger.log('Logger setup'); 102 | } 103 | connectOptions.server.auto_reconnect = null; 104 | MongoClient.connect(self.options.connectionString, connectOptions, connected); 105 | }else{ 106 | self.mongoClient = new MongoClient(connection); 107 | self.mongoClient.open(connected); 108 | } 109 | }; 110 | 111 | var connectReplSet = function(self, options, callback){ 112 | var l = options.servers.length, server, serverConfig, serverConnection; 113 | var servers = [], serverOptions = Options.ensure(options.serverOptions||{}, defaults.ServerOptions); 114 | for(var i = 0; i-1){ 266 | return self.createCollection(collectionName, collOptions, callback); 267 | } 268 | if(self.emitter.checkNoError(err, callback)){ 269 | if(!!capped){ 270 | if(typeof(callback)==='function'){ 271 | callback(null, collection); 272 | } 273 | }else{ 274 | collection.insert({workaround: 'This works around a bug with capping empty collections.'}, {safe:true}, function(){ 275 | self.db.command({"convertToCapped": collectionName, size: collOptions.size}, function(err, result){ 276 | if(self.emitter.checkNoError(err, callback)){ 277 | if(typeof(callback)==='function'){ 278 | if(self.emitter.checkNoError(err, callback)){ 279 | if (result.ok===0){ 280 | self.emitter.checkNoError(result.errmsg, callback); 281 | }else{ 282 | self.collection(collectionName, callback); 283 | } 284 | } 285 | } 286 | } 287 | }); 288 | }); 289 | } 290 | } 291 | }); 292 | } 293 | } 294 | }); 295 | } 296 | }); 297 | }; 298 | MongoConnection.prototype.ensureCapped.description = 'Ensures that the provided collection is capped.'; 299 | 300 | MongoConnection.prototype.tailedCursorStreamable = function(collectionName, filter, sort, callback){ 301 | var self = this; 302 | self.checkConnection(function(err){ 303 | if(self.emitter.checkNoError(err, callback)){ 304 | if(typeof(sort)==='function'){ 305 | callback = sort; 306 | sort = false; 307 | } 308 | if(typeof(callback)!=='function'){ 309 | throw new Error(errors.E_NOCALLBACK); 310 | }else if(typeof(filter)!=='object'){ 311 | return callback(new Error(errors.E_INVALIDFILTER)); 312 | }else{ 313 | self.collection(collectionName, function(err, collection){ 314 | if(self.emitter.checkNoError(err, callback)){ 315 | collection.isCapped(function(err, capped){ 316 | if(self.emitter.checkNoError(err, callback)){ 317 | if(capped){ 318 | var cursorOptions = {tailable: true}; 319 | if(sort) cursorOptions.sort = sort; 320 | callback(null, collection.find(filter, cursorOptions)); 321 | }else{ 322 | self.emitter.checkNoError(errors.E_INVALIDCURSORCOLLECTION, callback); 323 | } 324 | } 325 | }); 326 | } 327 | }); 328 | } 329 | } 330 | }); 331 | }; 332 | MongoConnection.prototype.tailedCursorStreamable.description = 'Returns a tailed cursor that can be used to create a stream that can be used to monitor the provided collection.'; 333 | 334 | MongoConnection.prototype.tailedCursorStream = function(collectionName, filter, sort, callback){ 335 | var self = this; 336 | self.checkConnection(function(err){ 337 | if(self.emitter.checkNoError(err, callback)){ 338 | if(typeof(sort)==='function'){ 339 | callback = sort; 340 | sort = false; 341 | } 342 | if(typeof(callback)!=='function'){ 343 | self.emitter.checkNoError(errors.E_NOCALLBACK); 344 | }else if(typeof(filter)!=='object'){ 345 | self.emitter.checkNoError(new Error(errors.E_INVALIDFILTER), callback); 346 | }else{ 347 | self.collection(collectionName, function(err, collection){ 348 | if(self.emitter.checkNoError(err, callback)){ 349 | collection.isCapped(function(err, capped){ 350 | if(self.emitter.checkNoError(err, callback)){ 351 | if(capped){ 352 | var cursorOptions = {tailable: true}; 353 | if(sort) cursorOptions.sort = sort; 354 | var cursor = collection.find(filter, cursorOptions); 355 | var stream = cursor.stream(); 356 | stream.cursor = cursor; 357 | callback(null, stream); 358 | }else{ 359 | self.emitter.checkNoError(errors.E_INVALIDCURSORCOLLECTION, callback); 360 | } 361 | } 362 | }); 363 | } 364 | }); 365 | } 366 | } 367 | }); 368 | }; 369 | MongoConnection.prototype.tailedCursorStream.description = 'Returns a tailed stream that can be used to monitor the provided collection.'; 370 | 371 | MongoConnection.prototype.tailedCursor = function(collectionName, filter, sort, callback){ 372 | var self = this; 373 | self.checkConnection(function(err){ 374 | if(self.emitter.checkNoError(err, callback)){ 375 | if(typeof(sort)==='function'){ 376 | callback = sort; 377 | sort = false; 378 | } 379 | if(typeof(callback)!=='function'){ 380 | self.emitter.checkNoError(errors.E_NOCALLBACK); 381 | }else if(typeof(filter)!=='object'){ 382 | self.emitter.checkNoError(errors.E_INVALIDFILTER, callback); 383 | }else{ 384 | self.collection(collectionName, function(err, collection){ 385 | if(self.emitter.checkNoError(err, callback)){ 386 | collection.isCapped(function(err, capped){ 387 | if(self.emitter.checkNoError(err, callback)){ 388 | if(capped){ 389 | var cursorOptions = {tailable: true}; 390 | if(sort) cursorOptions.sort = sort; 391 | collection.find(filter, cursorOptions, callback); 392 | }else{ 393 | self.emitter.checkNoError(errors.E_INVALIDCURSORCOLLECTION, callback); 394 | } 395 | } 396 | }); 397 | } 398 | }); 399 | } 400 | } 401 | }); 402 | }; 403 | MongoConnection.prototype.tailedCursor.description = 'Returns cursor that can be used with nextObject to monitor a collection.'; 404 | 405 | MongoConnection.prototype.writeGridFS = function(fileName, data, options, callback){ 406 | var self = this; 407 | if(typeof(options)==='function'){ 408 | callback = options; 409 | options = {}; 410 | } 411 | self.checkConnection(function(err){ 412 | if(self.emitter.checkNoError(err, callback)){ 413 | } 414 | }); 415 | }; 416 | 417 | MongoConnection.prototype.readGridFS = function(fileName, options, callback){ 418 | var self = this; 419 | if(typeof(options)==='function'){ 420 | callback = options; 421 | options = {}; 422 | } 423 | self.checkConnection(function(err){ 424 | if(self.emitter.checkNoError(err, callback)){ 425 | GridStore.exist(self.db, fileName, function(err, exists){ 426 | if(self.emitter.checkNoError(err, callback)){ 427 | if(exists===false){ 428 | callback(null, false); 429 | }else{ 430 | var gridStore = new GridStore(self.db, fileName, 'r'); 431 | gridStore.open(function(err, gridStore) { 432 | if(self.emitter.checkNoError(err, callback)){ 433 | gridStore.read(function(err){ 434 | if(self.emitter.checkNoError(err, callback)){ 435 | callback.apply(self, arguments); 436 | gridStore.close(function(){}); 437 | } 438 | }); 439 | } 440 | }); 441 | } 442 | } 443 | }); 444 | } 445 | }); 446 | }; 447 | MongoConnection.prototype.readGridFS.description = 'Retrieves the requested file from GridFS.'; 448 | 449 | MongoConnection.prototype.streamGridFS = function(fileName, callback){ 450 | var self = this; 451 | self.checkConnection(function(err){ 452 | if(self.emitter.checkNoError(err, callback)){ 453 | GridStore.exist(self.db, fileName, function(err, exists){ 454 | if(self.emitter.checkNoError(err, callback)){ 455 | if(!exists){ 456 | callback(null, false); 457 | }else{ 458 | var gridStore = new GridStore(self.db, fileName, 'r'); 459 | var doput = function(done){ 460 | return function(data){ 461 | if(data){ 462 | callback(null, data.toString()); 463 | } 464 | if(done){ 465 | callback(null, null); 466 | } 467 | }; 468 | }; 469 | var doerror = function(done){ 470 | return function(err){ 471 | self.emitter.checkNoError(err, callback); 472 | }; 473 | }; 474 | gridStore.open(function(err, gridStore) { 475 | var stream = gridStore.stream(true); 476 | stream.on('data', doput(false)); 477 | stream.on('error', doerror(true)); 478 | stream.on('end', doput(true)); 479 | }); 480 | } 481 | } 482 | }); 483 | } 484 | }); 485 | }; 486 | MongoConnection.prototype.streamGridFS.description = 'Retrieves the requested file from GridFS using streams to lower overhead.'; 487 | --------------------------------------------------------------------------------