You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
581 lines
14 KiB
581 lines
14 KiB
'use strict' |
|
|
|
var StringDecoder = require('string_decoder').StringDecoder |
|
var decoder = new StringDecoder() |
|
var ReplyError = require('./replyError') |
|
var ParserError = require('./parserError') |
|
var bufferPool = bufferAlloc(32 * 1024) |
|
var bufferOffset = 0 |
|
var interval = null |
|
var counter = 0 |
|
var notDecreased = 0 |
|
var isModern = typeof Buffer.allocUnsafe === 'function' |
|
|
|
/** |
|
* For backwards compatibility |
|
* @param len |
|
* @returns {Buffer} |
|
*/ |
|
|
|
function bufferAlloc (len) { |
|
return isModern ? Buffer.allocUnsafe(len) : new Buffer(len) |
|
} |
|
|
|
/** |
|
* Used for lengths and numbers only, faster perf on arrays / bulks |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseSimpleNumbers (parser) { |
|
var offset = parser.offset |
|
var length = parser.buffer.length - 1 |
|
var number = 0 |
|
var sign = 1 |
|
|
|
if (parser.buffer[offset] === 45) { |
|
sign = -1 |
|
offset++ |
|
} |
|
|
|
while (offset < length) { |
|
var c1 = parser.buffer[offset++] |
|
if (c1 === 13) { // \r\n |
|
parser.offset = offset + 1 |
|
return sign * number |
|
} |
|
number = (number * 10) + (c1 - 48) |
|
} |
|
} |
|
|
|
/** |
|
* Used for integer numbers in case of the returnNumbers option |
|
* |
|
* The maximimum possible integer to use is: Math.floor(Number.MAX_SAFE_INTEGER / 10) |
|
* Staying in a SMI Math.floor((Math.pow(2, 32) / 10) - 1) is even more efficient though |
|
* |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseStringNumbers (parser) { |
|
var offset = parser.offset |
|
var length = parser.buffer.length - 1 |
|
var number = 0 |
|
var res = '' |
|
|
|
if (parser.buffer[offset] === 45) { |
|
res += '-' |
|
offset++ |
|
} |
|
|
|
while (offset < length) { |
|
var c1 = parser.buffer[offset++] |
|
if (c1 === 13) { // \r\n |
|
parser.offset = offset + 1 |
|
if (number !== 0) { |
|
res += number |
|
} |
|
return res |
|
} else if (number > 429496728) { |
|
res += (number * 10) + (c1 - 48) |
|
number = 0 |
|
} else if (c1 === 48 && number === 0) { |
|
res += 0 |
|
} else { |
|
number = (number * 10) + (c1 - 48) |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Returns a string or buffer of the provided offset start and |
|
* end ranges. Checks `optionReturnBuffers`. |
|
* |
|
* If returnBuffers is active, all return values are returned as buffers besides numbers and errors |
|
* |
|
* @param parser |
|
* @param start |
|
* @param end |
|
* @returns {*} |
|
*/ |
|
function convertBufferRange (parser, start, end) { |
|
parser.offset = end + 2 |
|
if (parser.optionReturnBuffers === true) { |
|
return parser.buffer.slice(start, end) |
|
} |
|
|
|
return parser.buffer.toString('utf-8', start, end) |
|
} |
|
|
|
/** |
|
* Parse a '+' redis simple string response but forward the offsets |
|
* onto convertBufferRange to generate a string. |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseSimpleString (parser) { |
|
var start = parser.offset |
|
var offset = start |
|
var buffer = parser.buffer |
|
var length = buffer.length - 1 |
|
|
|
while (offset < length) { |
|
if (buffer[offset++] === 13) { // \r\n |
|
return convertBufferRange(parser, start, offset - 1) |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Returns the string length via parseSimpleNumbers |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseLength (parser) { |
|
var string = parseSimpleNumbers(parser) |
|
if (string !== undefined) { |
|
return string |
|
} |
|
} |
|
|
|
/** |
|
* Parse a ':' redis integer response |
|
* |
|
* If stringNumbers is activated the parser always returns numbers as string |
|
* This is important for big numbers (number > Math.pow(2, 53)) as js numbers |
|
* are 64bit floating point numbers with reduced precision |
|
* |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseInteger (parser) { |
|
if (parser.optionStringNumbers) { |
|
return parseStringNumbers(parser) |
|
} |
|
return parseSimpleNumbers(parser) |
|
} |
|
|
|
/** |
|
* Parse a '$' redis bulk string response |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseBulkString (parser) { |
|
var length = parseLength(parser) |
|
if (length === undefined) { |
|
return |
|
} |
|
if (length === -1) { |
|
return null |
|
} |
|
var offsetEnd = parser.offset + length |
|
if (offsetEnd + 2 > parser.buffer.length) { |
|
parser.bigStrSize = offsetEnd + 2 |
|
parser.bigOffset = parser.offset |
|
parser.totalChunkSize = parser.buffer.length |
|
parser.bufferCache.push(parser.buffer) |
|
return |
|
} |
|
|
|
return convertBufferRange(parser, parser.offset, offsetEnd) |
|
} |
|
|
|
/** |
|
* Parse a '-' redis error response |
|
* @param parser |
|
* @returns {Error} |
|
*/ |
|
function parseError (parser) { |
|
var string = parseSimpleString(parser) |
|
if (string !== undefined) { |
|
if (parser.optionReturnBuffers === true) { |
|
string = string.toString() |
|
} |
|
return new ReplyError(string) |
|
} |
|
} |
|
|
|
/** |
|
* Parsing error handler, resets parser buffer |
|
* @param parser |
|
* @param error |
|
*/ |
|
function handleError (parser, error) { |
|
parser.buffer = null |
|
parser.returnFatalError(error) |
|
} |
|
|
|
/** |
|
* Parse a '*' redis array response |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseArray (parser) { |
|
var length = parseLength(parser) |
|
if (length === undefined) { |
|
return |
|
} |
|
if (length === -1) { |
|
return null |
|
} |
|
var responses = new Array(length) |
|
return parseArrayElements(parser, responses, 0) |
|
} |
|
|
|
/** |
|
* Push a partly parsed array to the stack |
|
* |
|
* @param parser |
|
* @param elem |
|
* @param i |
|
* @returns {undefined} |
|
*/ |
|
function pushArrayCache (parser, elem, pos) { |
|
parser.arrayCache.push(elem) |
|
parser.arrayPos.push(pos) |
|
} |
|
|
|
/** |
|
* Parse chunked redis array response |
|
* @param parser |
|
* @returns {*} |
|
*/ |
|
function parseArrayChunks (parser) { |
|
var tmp = parser.arrayCache.pop() |
|
var pos = parser.arrayPos.pop() |
|
if (parser.arrayCache.length) { |
|
var res = parseArrayChunks(parser) |
|
if (!res) { |
|
pushArrayCache(parser, tmp, pos) |
|
return |
|
} |
|
tmp[pos++] = res |
|
} |
|
return parseArrayElements(parser, tmp, pos) |
|
} |
|
|
|
/** |
|
* Parse redis array response elements |
|
* @param parser |
|
* @param responses |
|
* @param i |
|
* @returns {*} |
|
*/ |
|
function parseArrayElements (parser, responses, i) { |
|
var bufferLength = parser.buffer.length |
|
while (i < responses.length) { |
|
var offset = parser.offset |
|
if (parser.offset >= bufferLength) { |
|
pushArrayCache(parser, responses, i) |
|
return |
|
} |
|
var response = parseType(parser, parser.buffer[parser.offset++]) |
|
if (response === undefined) { |
|
if (!parser.arrayCache.length) { |
|
parser.offset = offset |
|
} |
|
pushArrayCache(parser, responses, i) |
|
return |
|
} |
|
responses[i] = response |
|
i++ |
|
} |
|
|
|
return responses |
|
} |
|
|
|
/** |
|
* Called the appropriate parser for the specified type. |
|
* @param parser |
|
* @param type |
|
* @returns {*} |
|
*/ |
|
function parseType (parser, type) { |
|
switch (type) { |
|
case 36: // $ |
|
return parseBulkString(parser) |
|
case 58: // : |
|
return parseInteger(parser) |
|
case 43: // + |
|
return parseSimpleString(parser) |
|
case 42: // * |
|
return parseArray(parser) |
|
case 45: // - |
|
return parseError(parser) |
|
default: |
|
return handleError(parser, new ParserError( |
|
'Protocol error, got ' + JSON.stringify(String.fromCharCode(type)) + ' as reply type byte', |
|
JSON.stringify(parser.buffer), |
|
parser.offset |
|
)) |
|
} |
|
} |
|
|
|
// All allowed options including their typeof value |
|
var optionTypes = { |
|
returnError: 'function', |
|
returnFatalError: 'function', |
|
returnReply: 'function', |
|
returnBuffers: 'boolean', |
|
stringNumbers: 'boolean', |
|
name: 'string' |
|
} |
|
|
|
/** |
|
* Javascript Redis Parser |
|
* @param options |
|
* @constructor |
|
*/ |
|
function JavascriptRedisParser (options) { |
|
if (!(this instanceof JavascriptRedisParser)) { |
|
return new JavascriptRedisParser(options) |
|
} |
|
if (!options || !options.returnError || !options.returnReply) { |
|
throw new TypeError('Please provide all return functions while initiating the parser') |
|
} |
|
for (var key in options) { |
|
// eslint-disable-next-line valid-typeof |
|
if (optionTypes.hasOwnProperty(key) && typeof options[key] !== optionTypes[key]) { |
|
throw new TypeError('The options argument contains the property "' + key + '" that is either unknown or of a wrong type') |
|
} |
|
} |
|
if (options.name === 'hiredis') { |
|
/* istanbul ignore next: hiredis is only supported for legacy usage */ |
|
try { |
|
var Hiredis = require('./hiredis') |
|
console.error(new TypeError('Using hiredis is discouraged. Please use the faster JS parser by removing the name option.').stack.replace('Error', 'Warning')) |
|
return new Hiredis(options) |
|
} catch (e) { |
|
console.error(new TypeError('Hiredis is not installed. Please remove the `name` option. The (faster) JS parser is used instead.').stack.replace('Error', 'Warning')) |
|
} |
|
} |
|
this.optionReturnBuffers = !!options.returnBuffers |
|
this.optionStringNumbers = !!options.stringNumbers |
|
this.returnError = options.returnError |
|
this.returnFatalError = options.returnFatalError || options.returnError |
|
this.returnReply = options.returnReply |
|
this.name = 'javascript' |
|
this.reset() |
|
} |
|
|
|
/** |
|
* Reset the parser values to the initial state |
|
* |
|
* @returns {undefined} |
|
*/ |
|
JavascriptRedisParser.prototype.reset = function () { |
|
this.offset = 0 |
|
this.buffer = null |
|
this.bigStrSize = 0 |
|
this.bigOffset = 0 |
|
this.totalChunkSize = 0 |
|
this.bufferCache = [] |
|
this.arrayCache = [] |
|
this.arrayPos = [] |
|
} |
|
|
|
/** |
|
* Set the returnBuffers option |
|
* |
|
* @param returnBuffers |
|
* @returns {undefined} |
|
*/ |
|
JavascriptRedisParser.prototype.setReturnBuffers = function (returnBuffers) { |
|
if (typeof returnBuffers !== 'boolean') { |
|
throw new TypeError('The returnBuffers argument has to be a boolean') |
|
} |
|
this.optionReturnBuffers = returnBuffers |
|
} |
|
|
|
/** |
|
* Set the stringNumbers option |
|
* |
|
* @param stringNumbers |
|
* @returns {undefined} |
|
*/ |
|
JavascriptRedisParser.prototype.setStringNumbers = function (stringNumbers) { |
|
if (typeof stringNumbers !== 'boolean') { |
|
throw new TypeError('The stringNumbers argument has to be a boolean') |
|
} |
|
this.optionStringNumbers = stringNumbers |
|
} |
|
|
|
/** |
|
* Decrease the bufferPool size over time |
|
* @returns {undefined} |
|
*/ |
|
function decreaseBufferPool () { |
|
if (bufferPool.length > 50 * 1024) { |
|
// Balance between increasing and decreasing the bufferPool |
|
if (counter === 1 || notDecreased > counter * 2) { |
|
// Decrease the bufferPool by 10% by removing the first 10% of the current pool |
|
var sliceLength = Math.floor(bufferPool.length / 10) |
|
if (bufferOffset <= sliceLength) { |
|
bufferOffset = 0 |
|
} else { |
|
bufferOffset -= sliceLength |
|
} |
|
bufferPool = bufferPool.slice(sliceLength, bufferPool.length) |
|
} else { |
|
notDecreased++ |
|
counter-- |
|
} |
|
} else { |
|
clearInterval(interval) |
|
counter = 0 |
|
notDecreased = 0 |
|
interval = null |
|
} |
|
} |
|
|
|
/** |
|
* Check if the requested size fits in the current bufferPool. |
|
* If it does not, reset and increase the bufferPool accordingly. |
|
* |
|
* @param length |
|
* @returns {undefined} |
|
*/ |
|
function resizeBuffer (length) { |
|
if (bufferPool.length < length + bufferOffset) { |
|
var multiplier = length > 1024 * 1024 * 75 ? 2 : 3 |
|
if (bufferOffset > 1024 * 1024 * 111) { |
|
bufferOffset = 1024 * 1024 * 50 |
|
} |
|
bufferPool = bufferAlloc(length * multiplier + bufferOffset) |
|
bufferOffset = 0 |
|
counter++ |
|
if (interval === null) { |
|
interval = setInterval(decreaseBufferPool, 50) |
|
} |
|
} |
|
} |
|
|
|
/** |
|
* Concat a bulk string containing multiple chunks |
|
* |
|
* Notes: |
|
* 1) The first chunk might contain the whole bulk string including the \r |
|
* 2) We are only safe to fully add up elements that are neither the first nor any of the last two elements |
|
* |
|
* @param parser |
|
* @returns {String} |
|
*/ |
|
function concatBulkString (parser) { |
|
var list = parser.bufferCache |
|
var chunks = list.length |
|
var offset = parser.bigStrSize - parser.totalChunkSize |
|
parser.offset = offset |
|
if (offset <= 2) { |
|
if (chunks === 2) { |
|
return list[0].toString('utf8', parser.bigOffset, list[0].length + offset - 2) |
|
} |
|
chunks-- |
|
offset = list[list.length - 2].length + offset |
|
} |
|
var res = decoder.write(list[0].slice(parser.bigOffset)) |
|
for (var i = 1; i < chunks - 1; i++) { |
|
res += decoder.write(list[i]) |
|
} |
|
res += decoder.end(list[i].slice(0, offset - 2)) |
|
return res |
|
} |
|
|
|
/** |
|
* Concat the collected chunks from parser.bufferCache. |
|
* |
|
* Increases the bufferPool size beforehand if necessary. |
|
* |
|
* @param parser |
|
* @returns {Buffer} |
|
*/ |
|
function concatBulkBuffer (parser) { |
|
var list = parser.bufferCache |
|
var chunks = list.length |
|
var length = parser.bigStrSize - parser.bigOffset - 2 |
|
var offset = parser.bigStrSize - parser.totalChunkSize |
|
parser.offset = offset |
|
if (offset <= 2) { |
|
if (chunks === 2) { |
|
return list[0].slice(parser.bigOffset, list[0].length + offset - 2) |
|
} |
|
chunks-- |
|
offset = list[list.length - 2].length + offset |
|
} |
|
resizeBuffer(length) |
|
var start = bufferOffset |
|
list[0].copy(bufferPool, start, parser.bigOffset, list[0].length) |
|
bufferOffset += list[0].length - parser.bigOffset |
|
for (var i = 1; i < chunks - 1; i++) { |
|
list[i].copy(bufferPool, bufferOffset) |
|
bufferOffset += list[i].length |
|
} |
|
list[i].copy(bufferPool, bufferOffset, 0, offset - 2) |
|
bufferOffset += offset - 2 |
|
return bufferPool.slice(start, bufferOffset) |
|
} |
|
|
|
/** |
|
* Parse the redis buffer |
|
* @param buffer |
|
* @returns {undefined} |
|
*/ |
|
JavascriptRedisParser.prototype.execute = function execute (buffer) { |
|
if (this.buffer === null) { |
|
this.buffer = buffer |
|
this.offset = 0 |
|
} else if (this.bigStrSize === 0) { |
|
var oldLength = this.buffer.length |
|
var remainingLength = oldLength - this.offset |
|
var newBuffer = bufferAlloc(remainingLength + buffer.length) |
|
this.buffer.copy(newBuffer, 0, this.offset, oldLength) |
|
buffer.copy(newBuffer, remainingLength, 0, buffer.length) |
|
this.buffer = newBuffer |
|
this.offset = 0 |
|
if (this.arrayCache.length) { |
|
var arr = parseArrayChunks(this) |
|
if (!arr) { |
|
return |
|
} |
|
this.returnReply(arr) |
|
} |
|
} else if (this.totalChunkSize + buffer.length >= this.bigStrSize) { |
|
this.bufferCache.push(buffer) |
|
var tmp = this.optionReturnBuffers ? concatBulkBuffer(this) : concatBulkString(this) |
|
this.bigStrSize = 0 |
|
this.bufferCache = [] |
|
this.buffer = buffer |
|
if (this.arrayCache.length) { |
|
this.arrayCache[0][this.arrayPos[0]++] = tmp |
|
tmp = parseArrayChunks(this) |
|
if (!tmp) { |
|
return |
|
} |
|
} |
|
this.returnReply(tmp) |
|
} else { |
|
this.bufferCache.push(buffer) |
|
this.totalChunkSize += buffer.length |
|
return |
|
} |
|
|
|
while (this.offset < this.buffer.length) { |
|
var offset = this.offset |
|
var type = this.buffer[this.offset++] |
|
var response = parseType(this, type) |
|
if (response === undefined) { |
|
if (!this.arrayCache.length) { |
|
this.offset = offset |
|
} |
|
return |
|
} |
|
|
|
if (type === 45) { |
|
this.returnError(response) |
|
} else { |
|
this.returnReply(response) |
|
} |
|
} |
|
|
|
this.buffer = null |
|
} |
|
|
|
module.exports = JavascriptRedisParser
|
|
|