core
Version:
Core Libs
571 lines (490 loc) • 15.7 kB
JavaScript
/**
* @overview MemcachedSocket
* @copyright Dreamlab Onet.pl sp. z. o o 2012
*/
var Assertions = require('../common/Assertions.js'),
Types = require('../common/Types.js'),
EventDispatcher = require('../event/EventDispatcher.js').EventDispatcher,
Event = require('../event/Event.js').Event,
ErrorEvent = require('../event/ErrorEvent.js').ErrorEvent;
/**
* @class MemcachedSocket
* @classdesc Low-Level API to Memcached
* @extends EventDispatcher
* @param {Object} options
*/
var MemcachedSocket = exports.MemcachedSocket = function (options) {
EventDispatcher.call(this);
this._options = options;
this._socket = null;
this._socketBuffer = [];
this._operation = {};
this._head = null;
this._ending = new Buffer("\r\n", "binary");
this._ready = false;
//GLOBAL
this._headWritten = false;
this._totalSize = 0;
this._currentSize = 0;
this._currentChunk = 0;
//SET
this._totalChunks = 0;
this._doneChunks = 0;
this._waitForReplyAttached = false;
//GET
this._buffers = [];
this._buffersLength = 0;
this._onDataAttached = false;
};
MemcachedSocket.prototype = Object.create(EventDispatcher.prototype);
/**
* sets socket, if the socket is already connected READY event is emmited
* if not the ON_CONNECT listener is attached. When ON_CONNECT is emmited
* READY is emmited afterwards
* @method
* @params {Socket} socket
* @fires MemcachedSocket.Event.READY
*/
MemcachedSocket.prototype.setSocket = function (socket) {
this._socket = socket;
var self = this;
this.start();
if (this._socket._connected) {
this._flushBuffer();
this._ready = true;
this.dispatchEvent(new Event(MemcachedSocket.Event.READY, null));
} else {
this._socket.on("connect", function () {
self._flushBuffer();
self._ready = true;
self.dispatchEvent(new Event(MemcachedSocket.Event.READY, null));
});
}
};
/**
* and someone tries to addListener for READY event the event is immediately dispatched
* Overloaded EventDispatcher.addEventListener, if the class is in ready state
*/
MemcachedSocket.prototype.addEventListener = function (ev, fn, ctx) {
if (ev == MemcachedSocket.Event.READY && this._ready) {
var e = new Event(MemcachedSocket.Event.READY, null);
if (ctx) {
fn.apply(ctx, e);
} else {
fn(e);
}
return;
}
EventDispatcher.prototype.addEventListener.apply(this, arguments);
};
/**
* Initializes the memcache operation which is going to be exectued
* @method
* @param {String} method
* @param {String} key
* @param {Object} opts
*/
MemcachedSocket.prototype.init = function (method, key, opts) {
this._operation = {
method: method,
key: key,
opts: opts
};
};
/**
* Performs the given operation
* @method
* @fires MemcachedSocket.Event.ERROR
* @fires MemcachedSocket.Event.END
* @fires MemcachedSocket.Event.PROGRESS
*/
MemcachedSocket.prototype.start = function () {
var o = this._operation,
self = this,
key = o.key;
switch (o.method) {
case MemcachedSocket.Operation.SET:
this._totalSize = o.opts.length;
if (o.opts.chunkSize) {
//TODO: ta logike trzeba przeniesci w inne miejsce tak zeby tego nie liczyc co chunk
var chunks = o.opts.length / o.opts.chunkSize;
this._totalChunks = Math.ceil(chunks);
this._totalSize = o.opts.chunkSize;
this._currentChunk++;
if (chunks < this._totalChunks && this._currentChunk == this._totalChunks) {
this._totalSize = Math.floor((chunks - (this._totalChunks - 1)) * o.opts.chunkSize);
}
key = this._getChunkKey(key, this._currentChunk);
}
var headArgs = [MemcachedSocket.Operation.SET, key, 0, o.opts.lifetime, this._totalSize];
if (!o.opts.waitForReply) {
headArgs.push('noreply');
}
this._head = this._getHead(headArgs);
if (o.opts.waitForReply && !this._waitForReplyAttached) {
this._waitForReplyAttached = true;
this._socket.on('data', function (result) {
var r = self._parseResponse(result);
if (r.error) {
self.dispatchEvent(new ErrorEvent(MemcachedSocket.Event.ERROR, result, MemcachedSocket.Exception.UNKNOWN_ERROR, "UNKNOWN ERROR"));
self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
} else if (r.not_stored) {
self.dispatchEvent(new ErrorEvent(MemcachedSocket.Event.ERROR, result, MemcachedSocket.Exception.NOT_STORED, "NOT STORED"));
self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
} else if (r.stored) {
self._doneChunks += r.stored;
if (!o.opts.chunkSize || (o.opts.chunkSize && self._doneChunks == self._totalChunks)) {
self.dispatchEvent(new Event(MemcachedSocket.Event.END, true));
}
}
});
}
if (o.opts.data) {
if (o.opts.chunkSize) {
var c = (this._currentChunk - 1) * o.opts.chunkSize;
this.write(o.opts.data.slice(c, c + this._totalSize));
} else {
this.write(o.opts.data);
}
}
break;
case MemcachedSocket.Operation.GET:
var headRead = false,
emitEnd = false,
timeout = null;
if (o.opts.chunkSize) {
this._currentChunk++;
key = this._getChunkKey(key, this._currentChunk);
}
this._head = this._getHead([MemcachedSocket.Operation.GET, key]);
if (o.opts.timeout && (!o.opts.chunkSize || (o.opts.chunkSize && this._currentChunk == 1))) {
timeout = setTimeout(function () {
self.dispatchEvent(new ErrorEvent(MemcachedSocket.Event.ERROR, null, MemcachedSocket.Exception.TIMEDOUT, "TIMED OUT"));
self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
}, o.opts.timeout);
}
if (!this._onDataAttached) {
this._onDataAttached = true;
this._socket.on('data', function (buffer) {
var bLength = buffer.length;
if (!headRead) {
if (o.opts.timeout && (!o.opts.chunkSize || (o.opts.chunkSize && self._currentChunk == 1))) {
clearTimeout(timeout);
}
if (buffer[0] == 0x45) { //END
if (!o.opts.chunkSize || (o.opts.chunkSize && self._currentChunk == 1)) {
return self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
} else {
emitEnd = true;
}
} else if (buffer[0] == 0x42) { //BAD COMMAND FORMAT
self.dispatchEvent(new ErrorEvent(MemcachedSocket.Event.ERROR, buffer, MemcachedSocket.Exception.BAD_COMMAND_FORMAT, "BAD COMMAND FORMAT"));
return self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
} else {
for (var i = 0, l = bLength; i < l; i++) {
if (buffer[i] == 0x0d && buffer[i + 1] == 0x0a) {
buffer = buffer.slice(i + 2, bLength);
bLength -= i + 2;
break;
}
}
}
headRead = true;
}
if (!emitEnd) {
if (buffer[bLength - 1] == 0x0a && buffer[bLength - 2] == 0x0d && buffer[bLength - 5] == 0x45) {
bLength -= 7;
buffer = buffer.slice(0, bLength);
emitEnd = true;
}
//na wypadek gdyby END przyszedl w osobnym pakiecie
if (bLength > 0) {
self._buffers.push(buffer);
self._buffersLength += bLength;
self.dispatchEvent(new Event(MemcachedSocket.Event.PROGRESS, buffer));
}
if (o.opts.chunkSize && self._buffersLength % o.opts.chunkSize == 0) {
emitEnd = false;
headRead = false;
self.start();
return;
}
}
if (emitEnd) {
var data = new Buffer(self._buffersLength), p = 0;
for (var i = 0, l = self._buffers.length; i < l; i++) {
self._buffers[i].copy(data, p);
p += self._buffers[i].length;
}
self.dispatchEvent(new Event(MemcachedSocket.Event.END, data));
}
});
}
this._socket.write(this._head);
break;
case MemcachedSocket.Operation.DELETE:
var headArgs = [MemcachedSocket.Operation.DELETE, key, 0];
if (!o.opts.waitForReply) {
headArgs.push('noreply');
}
this._head = this._getHead(headArgs);
if (o.opts.waitForReply) {
this._socket.on('data', function (result) {
var r = self._parseResponse(result);
if (r.not_deleted) {
self.dispatchEvent(new ErrorEvent(MemcachedSocket.Event.ERROR, result, MemcachedSocket.Exception.NOT_DELETED, "NOT_DELETED"));
self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
} else if (r.error) {
self.dispatchEvent(new ErrorEvent(MemcachedSocket.Event.ERROR, result, MemcachedSocket.Exception.UNKNOWN_ERROR, "UNKNOWN ERROR"));
self.dispatchEvent(new Event(MemcachedSocket.Event.END, null));
} else {
self.dispatchEvent(new Event(MemcachedSocket.Event.END, true));
}
});
}
this.write(null, {
headOnly: true
});
break;
}
};
/**
* Writes data to the socket
* @method
* @param {Buffer|null} data
* @param {Object} opts
* @fires MemcachedSocket.Event.END
*/
MemcachedSocket.prototype.write = function (data, opts) {
var self = this,
opts = opts || {},
o = this._operation,
tmp = data,
dataSliced = false,
bytesSliced = 0;
if (!this._socket) {
this._socketBuffer.push(data);
return;
}
if (!this._headWritten) {
if (opts.headOnly) {
this._socket.write(this._head, "binary", function () {
if(!o.opts.waitForReply) {
process.nextTick(function () {
self.dispatchEvent(new Event(MemcachedSocket.Event.END, true));
});
}
});
} else {
this._socket.write(this._head);
}
this._headWritten = true;
}
if (data) {
if (this._currentSize + data.length > this._totalSize) {
bytesSliced = this._totalSize - this._currentSize;
tmp = data.slice(0, bytesSliced);
dataSliced = true;
}
this._socket.write(tmp);
this._currentSize += tmp.length;
}
if (this._currentSize == this._totalSize && !opts.headOnly) {
this._socket.write(this._ending, "binary", function () {
if(!o.opts.waitForReply) {
self._doneChunks++;
if (!o.opts.chunkSize || (o.opts.chunkSize && self._doneChunks == self._totalChunks)) {
self.dispatchEvent(new Event(MemcachedSocket.Event.END, true));
}
}
});
if (o.opts.chunkSize && this._currentChunk < this._totalChunks) {
this._currentSize = 0;
this._headWritten = false;
this.start();
}
}
if (dataSliced) {
this.write(data.slice(bytesSliced));
}
};
/**
* flushes the local buffer to the socket buffer,
* used to cache writes when there is no socket (has not been given yet or is awaiting for connection)
* @method
* @private
*/
MemcachedSocket.prototype._flushBuffer = function () {
var i = 0, l = this._socketBuffer.length;
for (; i < l; i++) {
this.write(this._socketBuffer[i]);
}
this._socketBuffer = [];
};
/**
* creates the memcached message header
* @method
* @private
* @params {Array} head
*/
MemcachedSocket.prototype._getHead = function (head) {
return new Buffer(head.join(' ') + "\r\n", "binary");
};
/**
* parses the memcache response
* @method
* @private
* @param {Buffer} buffer
* @returns {Object}
*/
MemcachedSocket.prototype._parseResponse = function (buffer) {
var data = buffer.toString().split("\r\n"),
i = 0,
l = data.length,
status = {
stored: 0,
deleted: 0,
not_stored: 0,
not_deleted: 0,
error: 0
};
for (; i < l; i++) {
switch (data[i]) {
case '':
break;
case 'STORED':
++status.stored;
break;
case 'DELETED':
++status.deleted;
break;
case 'NOT STORED':
++status.not_stored;
break;
case 'NOT DELETED':
++status.not_deleted;
break;
default:
++status.error;
break;
}
}
return status;
};
/**
* creates the chunked key
* @method
* @private
* @param {String} key
* @param {Number} chunk
* @returns {String}
*/
MemcachedSocket.prototype._getChunkKey = function (key, chunk) {
if (chunk == 1) {
return key;
} else {
return key + "_chunk_" + chunk;
}
};
/**
* @static
* @namespace
* @constant
*/
MemcachedSocket.Operation = {};
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Operation.SET = "set";
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Operation.GET = "get";
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Operation.DELETE = "delete";
/**
* @static
* @namespace
* @constant
*/
MemcachedSocket.Exception = {};
/**
* @static
* @constant
* @type {Number}
* @default
*/
MemcachedSocket.Exception.UNKNOWN_ERROR = -1;
/**
* @static
* @constant
* @type {Number}
* @default
*/
MemcachedSocket.Exception.TIMEDOUT = -2;
/**
* @static
* @constant
* @type {Number}
* @default
*/
MemcachedSocket.Exception.BAD_COMMAND_FORMAT = -3;
/**
* @static
* @constant
* @type {Number}
* @default
*/
MemcachedSocket.Exception.NOT_STORED = -4;
/**
* @static
* @constant
* @type {Number}
* @default
*/
MemcachedSocket.Exception.NOT_DELETED = -5;
/**
* @static
* @namespace
* @constant
*/
MemcachedSocket.Event = {};
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Event.READY = "MemcachedSocket_Event_READY";
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Event.PROGRESS = "MemcachedSocket_Event_PROGRESS";
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Event.ERROR = "MemcachedSocket_Event_ERROR";
/**
* @static
* @constant
* @type {String}
* @default
*/
MemcachedSocket.Event.END = "MemcachedSocket_Event_END";