| '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 |