@joshbetz/memcached
Version:
Memcached client for modern Node JS
205 lines (204 loc) • 7.99 kB
JavaScript
"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
Object.defineProperty(exports, "__esModule", { value: true });
const net_1 = require("net");
const events_1 = require("events");
class Memcached extends events_1.EventEmitter {
constructor(port, host, opts) {
super();
this.isReady = false;
this.opts = Object.assign({
prefix: '',
socketTimeout: 100,
}, opts);
// setup client
this.client = (0, net_1.createConnection)({ port, host });
this.client.once('connect', () => this.client.setTimeout(0));
this.client.once('ready', () => { this.isReady = true; });
this.client.setTimeout(this.opts.socketTimeout, () => {
this.emit('error', new Error('Socket Timeout'));
this.client.destroy();
});
// forward errors
this.client.on('error', (error) => {
this.emit('error', error);
});
}
ready() {
return __awaiter(this, void 0, void 0, function* () {
if (this.isReady) {
return true;
}
return new Promise((resolve, reject) => {
this.once('error', reject);
this.client.once('ready', resolve);
});
});
}
command(cmd, key, args = []) {
return __awaiter(this, void 0, void 0, function* () {
cmd = cmd.toLowerCase();
if (key) {
// keys cannot contain whitespace
key = key.replace(/\s+/, '_');
if (this.opts.prefix) {
key = this.opts.prefix + key;
}
if (key.length > 250) {
throw new Error('Invalid key');
}
args.unshift(key);
}
const command = `${cmd} ${args.join(' ')}\r\n`;
return new Promise((resolve, reject) => {
const isError = (data) => {
const errors = [
// error strings https://github.com/memcached/memcached/blob/master/doc/protocol.txt#L156
data.indexOf('ERROR\r\n'),
data.indexOf('CLIENT_ERROR'),
data.indexOf('SERVER_ERROR'),
];
return errors.some(token => token >= 0);
};
const onSimpleMessage = (data) => {
if (isError(data)) {
return reject(data);
}
return resolve(data.toString());
};
let buffer = '';
const onBufferedMessage = (data) => {
if (isError(data)) {
this.client.off('data', onBufferedMessage);
return reject(buffer);
}
buffer += data;
if (data.indexOf('END\r\n') < 0) {
// Keep looking for terminating tokens
return;
}
this.client.off('data', onBufferedMessage);
return resolve(buffer);
};
switch (cmd) {
case 'get':
case 'gets':
case 'gat':
case 'gats':
case 'mg':
case 'stat':
this.client.on('data', onBufferedMessage);
break;
default:
this.client.once('data', onSimpleMessage);
break;
}
this.client.write(command);
setTimeout(() => {
this.client.removeAllListeners();
reject(new Error('Command Timeout'));
}, Math.max(this.opts.socketTimeout, 1000)).unref();
});
});
}
flush() {
return __awaiter(this, void 0, void 0, function* () {
return this.command('flush_all');
});
}
store(command, key, value, ttl = 0) {
return __awaiter(this, void 0, void 0, function* () {
if (ttl > 60 * 60 * 24 * 30) {
// Memcached considers ttls over 30 days to be
// Unix timestamps. This is confusing and usually
// leads to bugs. Just error in this case.
throw new Error('Invalid TTL');
}
// Cast value to a string so we can take the length
value = value.toString();
const message = yield this.command(command, key, ['0', `${ttl}`, `${value.length}\r\n${value}`]);
if (message.indexOf('STORED') !== 0) {
return false;
}
return true;
});
}
set(key, value, ttl = 0) {
return __awaiter(this, void 0, void 0, function* () {
return this.store('set', key, value, ttl);
});
}
add(key, value, ttl = 0) {
return __awaiter(this, void 0, void 0, function* () {
return this.store('add', key, value, ttl);
});
}
get(key) {
return __awaiter(this, void 0, void 0, function* () {
const message = yield this.command('get', key);
if (message === 'END\r\n') {
return false;
}
// start after the \r\n
const start = message.indexOf('\r\n') + 2;
const end = message.indexOf('\r\nEND\r\n');
return message.substring(start, end);
});
}
del(key) {
return __awaiter(this, void 0, void 0, function* () {
const message = yield this.command('delete', key);
if (message.indexOf('DELETED') !== 0) {
return false;
}
return true;
});
}
incr(key, value = 1) {
return __awaiter(this, void 0, void 0, function* () {
const message = yield this.command('incr', key, [`${value}`]);
if (message === 'NOT_FOUND\r\n') {
return false;
}
const end = message.indexOf('\r\n');
return Number.parseInt(message.substring(0, end), 10);
});
}
decr(key, value = 1) {
return __awaiter(this, void 0, void 0, function* () {
const message = yield this.command('decr', key, [`${value}`]);
if (message === 'NOT_FOUND\r\n') {
return false;
}
const end = message.indexOf('\r\n');
return Number.parseInt(message.substring(0, end), 10);
});
}
ping() {
return __awaiter(this, void 0, void 0, function* () {
const message = yield this.command('version');
return message.indexOf('VERSION') === 0;
});
}
end() {
return __awaiter(this, void 0, void 0, function* () {
return new Promise(resolve => {
const timeout = setTimeout(() => this.client.destroy(), this.opts.socketTimeout);
this.client.once('close', () => {
clearTimeout(timeout);
resolve();
});
this.client.end();
});
});
}
}
exports.default = Memcached;