'use strict'; var ee2 = require('eventemitter2').EventEmitter2; var net = require('net'); var util = require('util'); var types = require('./connectionType.js'); var kestrel = function( options ){ this._settings = { servers: ['127.0.0.1:22133'], connectionType: types.ROUND_ROBIN, reconnect: false, reconnectDelay: 200 }; this._currentConnection = 0; this._connections = []; this._openConnection = null; if( options instanceof Object ){ for( var key in options ){ if( this._settings[key] !== undefined ){ this._settings[key] = options[key]; } } } this._pendingSetCallback = null; ee2.call(this); }; util.inherits(kestrel,ee2); //open connections to kestrel server(s) kestrel.prototype.connect = function(){ var type = this._settings.connectionType; if( types[type] === undefined || types[type] === null ){ throw 'Kestrel Client Connection: Argument 1 Must Be A Valid Kestrel Connection Type'; } var parts,port,host; switch( type ){ case types.RANDOM: case types.ROUND_ROBIN: for(var i in this._settings.servers){ parts = this._settings.servers[i].split(':'); port = (parts.length>1)?parts[1]:'22133'; host = parts[0]; this._connections.push( _createConnection(port,host,this) ); } break; case types.FAILOVER: var rand = Math.floor( Math.random()*this._settings.servers.length ); parts = this._settings.servers[rand].split(':'); port = (parts.length>1)?parts[1]:'22133'; host = parts[0]; this._connections.push( _createConnection(port,host,this) ); break; default: throw 'Kestrel Client Connection: Unknown Connection Type'; } }; function _createConnection(port, host, self){ var connection = net.connect(port,host); connection.on('error', function(err){ self._openConnection = null; self.emit('error', err); if(self._settings.reconnect){ setTimeout(function(){ self.connect(); },self._settings.reconnectDelay); } }); connection.on('data', function(data){ _handleData(data,self); self.emit('data', data); }); return connection; } function _handleData(data, self){ if( isResponse(data,'VALUE') ){ return _handleGet(data,self); } data = data.toString(); if( isResponse(data,'STORED') ) { self.emit('stored', true); if(self._pendingSetCallback){ var callback = self._pendingSetCallback; self._pendingSetCallback = null; callback(null); } } else if( isResponse(data,'STAT') ){ _handleStats(data,self); } else if( isResponse(data,'queue') ){ _handleDumpStats(data, self); } else if( isResponse(data,'DELETED\r\n') ){ self.emit('deleted', true); } else if( isResponse(data,'VERSION') ){ self.emit('version', data.split(' ')[1].replace('\r\n','')); } else if( isResponse(data,'Reloaded config.\r\n') ){ self.emit('reloaded', true); } else if( isResponse(data,'END\r\n') ){ self._openConnection = null; self.emit('empty', null); } else { console.log('unknown data from server',data, data.toString()); } } function isResponse(data,responseType){ var buffer = null; if(Buffer.isBuffer(data)){ buffer = data; } else { buffer = new Buffer(data,'utf8'); } return bufferStartsWithString(buffer,responseType); } function bufferStartsWithString(buffer,responseType){ var responseTypeAsBuffer = new Buffer(responseType,'utf8'); return bufferStartsWith(buffer,responseTypeAsBuffer); } function bufferStartsWith(buffer,prefixBuffer){ if(!buffer) return false; if( buffer.length < prefixBuffer.length){ return false; } for(var i = 0; i < prefixBuffer.length; i++) { if(prefixBuffer[i] != buffer[i]){ return false; } } return true; } function _handleGet(data, self){ var msg = {}; var header = parseValueHeader(data); msg.queue = header.queue; msg.data = data.slice(header.dataStart,header.dataEnd); self.emit('message', msg); } function parseValueHeader(data){ var endOfLineMarker = '\r\n'; var indexOfEndOfLineMarker = findEndOfLine(data,endOfLineMarker); if(indexOfEndOfLineMarker<=0){ throw new Error('incorrect \r\n in VALUE header'); } var headerString = data.slice(0,indexOfEndOfLineMarker).toString(); var headerParts = headerString.split(' '); if(headerParts[0] != 'VALUE'){ throw new Error('Invalid VALUE header', headerString); } var dataLength = parseInt(headerParts[3],10); var headerLength = indexOfEndOfLineMarker + endOfLineMarker.length; return { queue: headerParts[1], length: dataLength, dataStart: headerLength, dataEnd: headerLength+dataLength }; } function findEndOfLine(buffer,endOfLineMarker){ var eol = new Buffer(endOfLineMarker,'utf8'); var firstEolCharacter = eol[0]; if(buffer.length < eol.length){ return; } for (var i = 0; i < buffer.length-eol.length; i++) { if(buffer[i] !== firstEolCharacter){ continue; } var comparisonBuffer = buffer.slice(i,i+eol.length); console.log(i,eol,comparisonBuffer); if(bufferStartsWith(comparisonBuffer,eol)){ return i; } } return -1; } function _handleStats(data, self){ var stats = {}; var parts = data.match(/STAT ([a-zA-Z_]+) (.*?)\r\n/g); for( var i in parts ){ var part = parts[i].split(' '); stats[part[1]] = part[2].replace('\r\n', ''); } self.emit('stats', stats); } function _handleDumpStats(data, self){ var stats = {}; var parts = data.replace('\r\n}\r\nEND','').split('}\r\n'); for( var i in parts ){ var part = parts[i].split('\r\n'); var queue = part[0].match(/[a-zA-Z_]+/g)[1]; stats[queue] = {}; for( var lcv = 1; lcv < part.length-1; ++lcv ){ var p = part[lcv].split('='); stats[queue][ p[0].replace(/ /g,'') ] = p[1]; } } self.emit('dump_stats', stats); } kestrel.prototype._getConnection = function(){ if( this._connections.length == 1 ) { return this._connections[0]; } else if( this._connections.length === 0 ) { return null; } var connection = null; switch(this._settings.connectionType) { case types.RANDOM: var rand = Math.floor( Math.random()*this._connections.length ); connection = this._connections[rand]; break; case types.ROUND_ROBIN: this._currentConnection += 1; if( this._currentConnection > this._connections.length ){ this._currentConnection = 0; } connection = this._connections[this._currentConnection]; break; case types.FAILOVER: connection = this._connections[0]; break; } return connection; }; kestrel.prototype.close = function(){ //close any open connections for( var i in this._connections ){ this._connections[i].destroy(); } }; kestrel.prototype.set = function( queue, value, lifetime, callback){ if( typeof(lifetime) === 'function' ){ callback = lifetime; lifetime = null; } if( lifetime === undefined || lifetime === null ){ lifetime = 0; } if(this._pendingSetCallback && callback){ return callback('Cannot write again. Still waiting for previous write to be stored'); } var command = 'SET ' + queue + ' 0 ' + lifetime + ' '; command += value.length + '\r\n'; var connection = this._getConnection(); if( connection ){ if(callback){ this._pendingSetCallback = callback; } connection.write(command); connection.write(value); connection.write('\r\n'); } else { if(callback){ callback('No connection'); } else { throw new Error('No connection'); } } return this; }; kestrel.prototype.get = function(queue, timeout){ var command = 'GET ' + queue; timeout = parseInt(timeout,10); if( timeout > 0 ){ command += '/t='+timeout; } var connection = this._getConnection(); if( connection ){ connection.write(command + '\r\n'); } return this; }; kestrel.prototype.getNextOpen = function(queue, timeout){ var command = 'GET ' + queue; timeout = parseInt(timeout,10); if( timeout > 0 ){ command += '/t='+timeout; } var connection = this._openConnection; if(connection) { command += '/close'; } else { connection = this._getConnection(); this._openConnection = connection; } command += '/open'; if( connection ){ connection.write(command + '\r\n'); } return this; }; kestrel.prototype.delete = function(queue){ //delete given queue var connection = this._getConnection(); if( connection ){ connection.write('DELETE ' + queue + '\r\n'); } return this; }; kestrel.prototype.flush = function(queue){ //flush given queue var connection = this._getConnection(); if( connection ){ connection.write('FLUSH ' + queue + '\r\n'); } return this; }; kestrel.prototype.flushAll = function(){ //flush all queues var connection = this._getConnection(); if( connection ){ connection.write('FLUSH_ALL\r\n'); } return this; }; kestrel.prototype.version = function(){ //get version of server var connection = this._getConnection(); if( connection ){ connection.write('VERSION\r\n'); } return this; }; kestrel.prototype.shutdown = function(){ //shutdown server var connection = this._getConnection(); if( connection ){ connection.write('SHUTDOWN\r\n'); } return this; }; kestrel.prototype.reload = function(){ //reload the server var connection = this._getConnection(); if( connection ){ connection.write('RELOAD\r\n'); } return this; }; kestrel.prototype.stats = function(){ //get server stats var connection = this._getConnection(); if( connection ){ connection.write('STATS\r\n'); } return this; }; kestrel.prototype.dumpStats = function(){ //dump server stats var connection = this._getConnection(); if( connection ){ connection.write('DUMP_STATS\r\n'); } return this; }; kestrel.prototype.monitor = function(queue, seconds, maxItems){ //monitor the given queue }; kestrel.prototype.confirm = function(queue, count){ //confirm received items }; module.exports = kestrel;