mirror of
https://codeberg.org/tacerus/teddit.git
synced 2025-01-09 12:52:32 +01:00
553 lines
14 KiB
JavaScript
553 lines
14 KiB
JavaScript
'use strict'
|
|
|
|
const Buffer = require('buffer').Buffer
|
|
const StringDecoder = require('string_decoder').StringDecoder
|
|
const decoder = new StringDecoder()
|
|
const errors = require('redis-errors')
|
|
const ReplyError = errors.ReplyError
|
|
const ParserError = errors.ParserError
|
|
var bufferPool = Buffer.allocUnsafe(32 * 1024)
|
|
var bufferOffset = 0
|
|
var interval = null
|
|
var counter = 0
|
|
var notDecreased = 0
|
|
|
|
/**
|
|
* Used for integer numbers only
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|number}
|
|
*/
|
|
function parseSimpleNumbers (parser) {
|
|
const length = parser.buffer.length - 1
|
|
var offset = parser.offset
|
|
var number = 0
|
|
var sign = 1
|
|
|
|
if (parser.buffer[offset] === 45) {
|
|
sign = -1
|
|
offset++
|
|
}
|
|
|
|
while (offset < length) {
|
|
const 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
|
|
*
|
|
* Reading the string as parts of n SMI is more efficient than
|
|
* using a string directly.
|
|
*
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|string}
|
|
*/
|
|
function parseStringNumbers (parser) {
|
|
const length = parser.buffer.length - 1
|
|
var offset = parser.offset
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parse a '+' redis simple string response but forward the offsets
|
|
* onto convertBufferRange to generate a string.
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|string|Buffer}
|
|
*/
|
|
function parseSimpleString (parser) {
|
|
const start = parser.offset
|
|
const buffer = parser.buffer
|
|
const length = buffer.length - 1
|
|
var offset = start
|
|
|
|
while (offset < length) {
|
|
if (buffer[offset++] === 13) { // \r\n
|
|
parser.offset = offset + 1
|
|
if (parser.optionReturnBuffers === true) {
|
|
return parser.buffer.slice(start, offset - 1)
|
|
}
|
|
return parser.buffer.toString('utf8', start, offset - 1)
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the read length
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|number}
|
|
*/
|
|
function parseLength (parser) {
|
|
const length = parser.buffer.length - 1
|
|
var offset = parser.offset
|
|
var number = 0
|
|
|
|
while (offset < length) {
|
|
const c1 = parser.buffer[offset++]
|
|
if (c1 === 13) {
|
|
parser.offset = offset + 1
|
|
return number
|
|
}
|
|
number = (number * 10) + (c1 - 48)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 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 {JavascriptRedisParser} parser
|
|
* @returns {undefined|number|string}
|
|
*/
|
|
function parseInteger (parser) {
|
|
if (parser.optionStringNumbers === true) {
|
|
return parseStringNumbers(parser)
|
|
}
|
|
return parseSimpleNumbers(parser)
|
|
}
|
|
|
|
/**
|
|
* Parse a '$' redis bulk string response
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|null|string}
|
|
*/
|
|
function parseBulkString (parser) {
|
|
const length = parseLength(parser)
|
|
if (length === undefined) {
|
|
return
|
|
}
|
|
if (length < 0) {
|
|
return null
|
|
}
|
|
const offset = parser.offset + length
|
|
if (offset + 2 > parser.buffer.length) {
|
|
parser.bigStrSize = offset + 2
|
|
parser.totalChunkSize = parser.buffer.length
|
|
parser.bufferCache.push(parser.buffer)
|
|
return
|
|
}
|
|
const start = parser.offset
|
|
parser.offset = offset + 2
|
|
if (parser.optionReturnBuffers === true) {
|
|
return parser.buffer.slice(start, offset)
|
|
}
|
|
return parser.buffer.toString('utf8', start, offset)
|
|
}
|
|
|
|
/**
|
|
* Parse a '-' redis error response
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {ReplyError}
|
|
*/
|
|
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 {JavascriptRedisParser} parser
|
|
* @param {number} type
|
|
* @returns {undefined}
|
|
*/
|
|
function handleError (parser, type) {
|
|
const err = new ParserError(
|
|
'Protocol error, got ' + JSON.stringify(String.fromCharCode(type)) + ' as reply type byte',
|
|
JSON.stringify(parser.buffer),
|
|
parser.offset
|
|
)
|
|
parser.buffer = null
|
|
parser.returnFatalError(err)
|
|
}
|
|
|
|
/**
|
|
* Parse a '*' redis array response
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|null|any[]}
|
|
*/
|
|
function parseArray (parser) {
|
|
const length = parseLength(parser)
|
|
if (length === undefined) {
|
|
return
|
|
}
|
|
if (length < 0) {
|
|
return null
|
|
}
|
|
const responses = new Array(length)
|
|
return parseArrayElements(parser, responses, 0)
|
|
}
|
|
|
|
/**
|
|
* Push a partly parsed array to the stack
|
|
*
|
|
* @param {JavascriptRedisParser} parser
|
|
* @param {any[]} array
|
|
* @param {number} pos
|
|
* @returns {undefined}
|
|
*/
|
|
function pushArrayCache (parser, array, pos) {
|
|
parser.arrayCache.push(array)
|
|
parser.arrayPos.push(pos)
|
|
}
|
|
|
|
/**
|
|
* Parse chunked redis array response
|
|
* @param {JavascriptRedisParser} parser
|
|
* @returns {undefined|any[]}
|
|
*/
|
|
function parseArrayChunks (parser) {
|
|
const tmp = parser.arrayCache.pop()
|
|
var pos = parser.arrayPos.pop()
|
|
if (parser.arrayCache.length) {
|
|
const res = parseArrayChunks(parser)
|
|
if (res === undefined) {
|
|
pushArrayCache(parser, tmp, pos)
|
|
return
|
|
}
|
|
tmp[pos++] = res
|
|
}
|
|
return parseArrayElements(parser, tmp, pos)
|
|
}
|
|
|
|
/**
|
|
* Parse redis array response elements
|
|
* @param {JavascriptRedisParser} parser
|
|
* @param {Array} responses
|
|
* @param {number} i
|
|
* @returns {undefined|null|any[]}
|
|
*/
|
|
function parseArrayElements (parser, responses, i) {
|
|
const bufferLength = parser.buffer.length
|
|
while (i < responses.length) {
|
|
const offset = parser.offset
|
|
if (parser.offset >= bufferLength) {
|
|
pushArrayCache(parser, responses, i)
|
|
return
|
|
}
|
|
const response = parseType(parser, parser.buffer[parser.offset++])
|
|
if (response === undefined) {
|
|
if (!(parser.arrayCache.length || parser.bufferCache.length)) {
|
|
parser.offset = offset
|
|
}
|
|
pushArrayCache(parser, responses, i)
|
|
return
|
|
}
|
|
responses[i] = response
|
|
i++
|
|
}
|
|
|
|
return responses
|
|
}
|
|
|
|
/**
|
|
* Called the appropriate parser for the specified type.
|
|
*
|
|
* 36: $
|
|
* 43: +
|
|
* 42: *
|
|
* 58: :
|
|
* 45: -
|
|
*
|
|
* @param {JavascriptRedisParser} parser
|
|
* @param {number} type
|
|
* @returns {*}
|
|
*/
|
|
function parseType (parser, type) {
|
|
switch (type) {
|
|
case 36:
|
|
return parseBulkString(parser)
|
|
case 43:
|
|
return parseSimpleString(parser)
|
|
case 42:
|
|
return parseArray(parser)
|
|
case 58:
|
|
return parseInteger(parser)
|
|
case 45:
|
|
return parseError(parser)
|
|
default:
|
|
return handleError(parser, type)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Decrease the bufferPool size over time
|
|
*
|
|
* Balance between increasing and decreasing the bufferPool.
|
|
* Decrease the bufferPool by 10% by removing the first 10% of the current pool.
|
|
* @returns {undefined}
|
|
*/
|
|
function decreaseBufferPool () {
|
|
if (bufferPool.length > 50 * 1024) {
|
|
if (counter === 1 || notDecreased > counter * 2) {
|
|
const minSliceLen = Math.floor(bufferPool.length / 10)
|
|
const sliceLength = minSliceLen < bufferOffset
|
|
? bufferOffset
|
|
: minSliceLen
|
|
bufferOffset = 0
|
|
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 {number} length
|
|
* @returns {undefined}
|
|
*/
|
|
function resizeBuffer (length) {
|
|
if (bufferPool.length < length + bufferOffset) {
|
|
const multiplier = length > 1024 * 1024 * 75 ? 2 : 3
|
|
if (bufferOffset > 1024 * 1024 * 111) {
|
|
bufferOffset = 1024 * 1024 * 50
|
|
}
|
|
bufferPool = Buffer.allocUnsafe(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 {JavascriptRedisParser} parser
|
|
* @returns {String}
|
|
*/
|
|
function concatBulkString (parser) {
|
|
const list = parser.bufferCache
|
|
const oldOffset = parser.offset
|
|
var chunks = list.length
|
|
var offset = parser.bigStrSize - parser.totalChunkSize
|
|
parser.offset = offset
|
|
if (offset <= 2) {
|
|
if (chunks === 2) {
|
|
return list[0].toString('utf8', oldOffset, list[0].length + offset - 2)
|
|
}
|
|
chunks--
|
|
offset = list[list.length - 2].length + offset
|
|
}
|
|
var res = decoder.write(list[0].slice(oldOffset))
|
|
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 {JavascriptRedisParser} parser
|
|
* @returns {Buffer}
|
|
*/
|
|
function concatBulkBuffer (parser) {
|
|
const list = parser.bufferCache
|
|
const oldOffset = parser.offset
|
|
const length = parser.bigStrSize - oldOffset - 2
|
|
var chunks = list.length
|
|
var offset = parser.bigStrSize - parser.totalChunkSize
|
|
parser.offset = offset
|
|
if (offset <= 2) {
|
|
if (chunks === 2) {
|
|
return list[0].slice(oldOffset, list[0].length + offset - 2)
|
|
}
|
|
chunks--
|
|
offset = list[list.length - 2].length + offset
|
|
}
|
|
resizeBuffer(length)
|
|
const start = bufferOffset
|
|
list[0].copy(bufferPool, start, oldOffset, list[0].length)
|
|
bufferOffset += list[0].length - oldOffset
|
|
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)
|
|
}
|
|
|
|
class JavascriptRedisParser {
|
|
/**
|
|
* Javascript Redis Parser constructor
|
|
* @param {{returnError: Function, returnReply: Function, returnFatalError?: Function, returnBuffers: boolean, stringNumbers: boolean }} options
|
|
* @constructor
|
|
*/
|
|
constructor (options) {
|
|
if (!options) {
|
|
throw new TypeError('Options are mandatory.')
|
|
}
|
|
if (typeof options.returnError !== 'function' || typeof options.returnReply !== 'function') {
|
|
throw new TypeError('The returnReply and returnError options have to be functions.')
|
|
}
|
|
this.setReturnBuffers(!!options.returnBuffers)
|
|
this.setStringNumbers(!!options.stringNumbers)
|
|
this.returnError = options.returnError
|
|
this.returnFatalError = options.returnFatalError || options.returnError
|
|
this.returnReply = options.returnReply
|
|
this.reset()
|
|
}
|
|
|
|
/**
|
|
* Reset the parser values to the initial state
|
|
*
|
|
* @returns {undefined}
|
|
*/
|
|
reset () {
|
|
this.offset = 0
|
|
this.buffer = null
|
|
this.bigStrSize = 0
|
|
this.totalChunkSize = 0
|
|
this.bufferCache = []
|
|
this.arrayCache = []
|
|
this.arrayPos = []
|
|
}
|
|
|
|
/**
|
|
* Set the returnBuffers option
|
|
*
|
|
* @param {boolean} returnBuffers
|
|
* @returns {undefined}
|
|
*/
|
|
setReturnBuffers (returnBuffers) {
|
|
if (typeof returnBuffers !== 'boolean') {
|
|
throw new TypeError('The returnBuffers argument has to be a boolean')
|
|
}
|
|
this.optionReturnBuffers = returnBuffers
|
|
}
|
|
|
|
/**
|
|
* Set the stringNumbers option
|
|
*
|
|
* @param {boolean} stringNumbers
|
|
* @returns {undefined}
|
|
*/
|
|
setStringNumbers (stringNumbers) {
|
|
if (typeof stringNumbers !== 'boolean') {
|
|
throw new TypeError('The stringNumbers argument has to be a boolean')
|
|
}
|
|
this.optionStringNumbers = stringNumbers
|
|
}
|
|
|
|
/**
|
|
* Parse the redis buffer
|
|
* @param {Buffer} buffer
|
|
* @returns {undefined}
|
|
*/
|
|
execute (buffer) {
|
|
if (this.buffer === null) {
|
|
this.buffer = buffer
|
|
this.offset = 0
|
|
} else if (this.bigStrSize === 0) {
|
|
const oldLength = this.buffer.length
|
|
const remainingLength = oldLength - this.offset
|
|
const newBuffer = Buffer.allocUnsafe(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) {
|
|
const arr = parseArrayChunks(this)
|
|
if (arr === undefined) {
|
|
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 === undefined) {
|
|
return
|
|
}
|
|
}
|
|
this.returnReply(tmp)
|
|
} else {
|
|
this.bufferCache.push(buffer)
|
|
this.totalChunkSize += buffer.length
|
|
return
|
|
}
|
|
|
|
while (this.offset < this.buffer.length) {
|
|
const offset = this.offset
|
|
const type = this.buffer[this.offset++]
|
|
const response = parseType(this, type)
|
|
if (response === undefined) {
|
|
if (!(this.arrayCache.length || this.bufferCache.length)) {
|
|
this.offset = offset
|
|
}
|
|
return
|
|
}
|
|
|
|
if (type === 45) {
|
|
this.returnError(response)
|
|
} else {
|
|
this.returnReply(response)
|
|
}
|
|
}
|
|
|
|
this.buffer = null
|
|
}
|
|
}
|
|
|
|
module.exports = JavascriptRedisParser
|