fivebeans
Version:
beanstalkd client & worker daemon for node.
345 lines (295 loc) • 8.79 kB
JavaScript
var
events = require('events'),
net = require('net'),
util = require('util'),
yaml = require('js-yaml')
;
var DEFAULT_HOST = '127.0.0.1';
var DEFAULT_PORT = 11300;
var LOWEST_PRIORITY = 1000;
// utilities
// Turn a function argument hash into an array for slicing.
function argHashToArray(hash)
{
var keys = Object.keys(hash);
var result = [];
for (var i = 0; i < keys.length; i++)
{
result[parseInt(keys[i], 10)] = hash[keys[i]];
}
return result;
}
var FiveBeansClient = function(host, port)
{
events.EventEmitter.call(this);
this.stream = null;
this.handlers = [];
this.buffer = undefined;
this.host = host ? host : DEFAULT_HOST;
this.port = port ? port : DEFAULT_PORT;
};
util.inherits(FiveBeansClient, events.EventEmitter);
FiveBeansClient.prototype.connect = function()
{
var self = this, tmp;
self.stream = net.createConnection(self.port, self.host);
self.stream.on('data', function(data)
{
if (!self.buffer)
self.buffer = data;
else
{
tmp = new Buffer(self.buffer.length + data.length);
self.buffer.copy(tmp, 0);
data.copy(tmp, self.buffer.length);
self.buffer = tmp;
}
self.tryHandlingResponse();
});
self.stream.on('connect', function()
{
self.emit('connect');
});
self.stream.on('error', function(err)
{
self.emit('error', err);
});
self.stream.on('close', function(err)
{
self.emit('close', err);
});
};
FiveBeansClient.prototype.end = function()
{
if (this.stream)
this.stream.end();
};
FiveBeansClient.prototype.tryHandlingResponse = function()
{
while (true)
{
// Peek at the oldest handler in our list and see if if thinks it's done.
var latest = this.handlers[0];
if (!latest) break;
var handler = latest[0];
var callback = latest[1];
if ((handler !== undefined) && (handler !== null))
{
this.buffer = handler.process(this.buffer);
if (handler.complete)
{
// shift it off & reset
this.handlers.shift();
if (handler.success)
callback && callback.call.apply(callback,
[null, null].concat(handler.args));
else
callback && callback.call(null,
handler.args[0]);
if (typeof handler.remainder !== 'undefined')
{
this.buffer = handler.remainder;
}
}
else
{
handler.reset();
break;
}
}
else
{
break;
}
}
};
// response handlers
var ResponseHandler = function(expectedResponse)
{
this.expectedResponse = expectedResponse;
return this;
};
ResponseHandler.prototype.reset = function()
{
this.complete = false;
this.success = false;
this.args = undefined;
this.header = undefined;
this.body = undefined;
};
ResponseHandler.prototype.RESPONSES_REQUIRING_BODY =
{
RESERVED: 'passthrough',
FOUND: 'passthrough',
OK: 'yaml'
};
function findInBuffer(buffer, bytes)
{
var ptr = 0, idx = 0;
while (ptr < buffer.length)
{
if (buffer[ptr] === bytes[idx])
{
idx++;
if (idx === bytes.length)
return (ptr - bytes.length + 1);
}
else
idx = 0;
ptr++;
}
return -1;
}
var CRLF = new Buffer([0x0d, 0x0a]);
ResponseHandler.prototype.process = function(data)
{
var eol = findInBuffer(data, CRLF);
// afaict this is an eslint 2.8.0 bug: it complains about this brace
/*eslint brace-style:0*/
if(eol > -1)
{
var sliceStart;
// Header is everything up to the windows line break;
// body is everything after.
this.header = data.toString('utf8', 0, eol);
this.body = data.slice(eol + 2, data.length);
this.args = this.header.split(' ');
var response = this.args[0];
if (response === this.expectedResponse)
{
this.success = true;
this.args.shift(); // remove it as redundant
}
if (this.RESPONSES_REQUIRING_BODY[response])
{
this.parseBody(this.RESPONSES_REQUIRING_BODY[response]);
if (this.complete)
{
sliceStart = eol + 2 + data.length + 2;
if (sliceStart >= data.length)
return new Buffer(0);
return data.slice(eol + 2 + data.length + 2);
}
}
else
{
this.complete = true;
sliceStart = eol + 2;
if (sliceStart >= data.length)
return new Buffer(0);
return data.slice(eol + 2);
}
}
else {
// no response expected (quit)
if ('' === this.expectedResponse)
{
this.success = true;
this.complete = true;
}
}
return data;
};
/*
RESERVED <id> <bytes>\r\n
<data>\r\n
OK <bytes>\r\n
<data>\r\n
Beanstalkd commands like reserve() & stats() return a body.
We must read <bytes> data in response.
*/
ResponseHandler.prototype.parseBody = function(how)
{
if ((this.body === undefined) || (this.body === null))
return;
var expectedLength = parseInt(this.args[this.args.length - 1], 10);
if (this.body.length > (expectedLength + 2))
{
// Body contains multiple responses. Split off the remaining bytes.
this.remainder = this.body.slice(expectedLength + 2);
this.body = this.body.slice(0, expectedLength + 2);
}
if (this.body.length === (expectedLength + 2))
{
this.args.pop();
var body = this.body.slice(0, expectedLength);
this.complete = true;
switch (how)
{
case 'yaml':
this.args.push(yaml.load(body.toString()));
break;
// case 'passthrough':
default:
this.args.push(body);
break;
}
}
};
// Implementing the beanstalkd interface.
function makeBeanstalkCommand(command, expectedResponse, sendsData)
{
// Commands are called as client.COMMAND(arg1, arg2, ... data, callback);
// They're sent to beanstalkd as: COMMAND arg1 arg2 ...
// followed by data.
// So we slice the callback & data from the passed-in arguments, prepend
// the command, then send the arglist otherwise intact.
// We then push a handler for the expected response onto our handler stack.
// Some commands have no args, just a callback (stats, stats-tube, etc);
// That's the case handled when args < 2.
return function()
{
var data,
buffer,
args = argHashToArray(arguments),
callback = args.pop();
args.unshift(command);
if (sendsData)
{
data = args.pop();
if (!Buffer.isBuffer(data))
data = new Buffer(data);
args.push(data.length);
}
this.handlers.push([new ResponseHandler(expectedResponse), callback]);
if (data)
{
buffer = Buffer.concat([new Buffer(args.join(' ')), CRLF, data, CRLF]);
}
else
{
buffer = Buffer.concat([new Buffer(args.join(' ')), CRLF]);
}
this.stream.write(buffer);
};
}
// beanstalkd commands
FiveBeansClient.prototype.use = makeBeanstalkCommand('use', 'USING');
FiveBeansClient.prototype.put = makeBeanstalkCommand('put', 'INSERTED', true);
FiveBeansClient.prototype.watch = makeBeanstalkCommand('watch', 'WATCHING');
FiveBeansClient.prototype.ignore = makeBeanstalkCommand('ignore', 'WATCHING');
FiveBeansClient.prototype.reserve = makeBeanstalkCommand('reserve', 'RESERVED');
FiveBeansClient.prototype.reserve_with_timeout = makeBeanstalkCommand('reserve-with-timeout', 'RESERVED');
FiveBeansClient.prototype.destroy = makeBeanstalkCommand('delete', 'DELETED');
FiveBeansClient.prototype.release = makeBeanstalkCommand('release', 'RELEASED');
FiveBeansClient.prototype.bury = makeBeanstalkCommand('bury', 'BURIED');
FiveBeansClient.prototype.touch = makeBeanstalkCommand('touch', 'TOUCHED');
FiveBeansClient.prototype.kick = makeBeanstalkCommand('kick', 'KICKED');
FiveBeansClient.prototype.kick_job = makeBeanstalkCommand('kick-job', 'KICKED');
FiveBeansClient.prototype.peek = makeBeanstalkCommand('peek', 'FOUND');
FiveBeansClient.prototype.peek_ready = makeBeanstalkCommand('peek-ready', 'FOUND');
FiveBeansClient.prototype.peek_delayed = makeBeanstalkCommand('peek-delayed', 'FOUND');
FiveBeansClient.prototype.peek_buried = makeBeanstalkCommand('peek-buried', 'FOUND');
FiveBeansClient.prototype.list_tube_used = makeBeanstalkCommand('list-tube-used', 'USING');
FiveBeansClient.prototype.pause_tube = makeBeanstalkCommand('pause-tube', 'PAUSED');
// the server returns yaml files in response to these commands
FiveBeansClient.prototype.list_tubes = makeBeanstalkCommand('list-tubes', 'OK');
FiveBeansClient.prototype.list_tubes_watched = makeBeanstalkCommand('list-tubes-watched', 'OK');
FiveBeansClient.prototype.stats_job = makeBeanstalkCommand('stats-job', 'OK');
FiveBeansClient.prototype.stats_tube = makeBeanstalkCommand('stats-tube', 'OK');
FiveBeansClient.prototype.stats = makeBeanstalkCommand('stats', 'OK');
// closes the connection, no response
FiveBeansClient.prototype.quit = makeBeanstalkCommand('quit', '');
// end beanstalkd commands
module.exports = FiveBeansClient;
FiveBeansClient.LOWEST_PRIORITY = LOWEST_PRIORITY;