2016-11-29 18:29:50 -08:00
'use strict'
var StringDecoder = require ( 'string_decoder' ) . StringDecoder
var decoder = new StringDecoder ( )
var ReplyError = require ( './replyError' )
2019-04-13 16:01:51 -04:00
var ParserError = require ( './parserError' )
var bufferPool = bufferAlloc ( 32 * 1024 )
2016-11-29 18:29:50 -08:00
var bufferOffset = 0
var interval = null
var counter = 0
var notDecreased = 0
2019-04-13 16:01:51 -04:00
var isModern = typeof Buffer . allocUnsafe === 'function'
/ * *
* For backwards compatibility
* @ param len
* @ returns { Buffer }
* /
function bufferAlloc ( len ) {
return isModern ? Buffer . allocUnsafe ( len ) : new Buffer ( len )
}
2016-11-29 18:29:50 -08:00
/ * *
* 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 { * }
* /
2019-04-13 16:01:51 -04:00
function parseSimpleString ( parser ) {
2016-11-29 18:29:50 -08:00
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 64 bit 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 ) {
2019-04-13 16:01:51 -04:00
var string = parseSimpleString ( parser )
2016-11-29 18:29:50 -08:00
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 )
}
2019-04-13 16:01:51 -04:00
/ * *
* 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 )
}
2016-11-29 18:29:50 -08:00
/ * *
* Parse chunked redis array response
* @ param parser
* @ returns { * }
* /
function parseArrayChunks ( parser ) {
2019-04-13 16:01:51 -04:00
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 )
2016-11-29 18:29:50 -08:00
}
/ * *
* 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 ) {
2019-04-13 16:01:51 -04:00
pushArrayCache ( parser , responses , i )
2016-11-29 18:29:50 -08:00
return
}
var response = parseType ( parser , parser . buffer [ parser . offset ++ ] )
if ( response === undefined ) {
2019-04-13 16:01:51 -04:00
if ( ! parser . arrayCache . length ) {
parser . offset = offset
}
pushArrayCache ( parser , responses , i )
2016-11-29 18:29:50 -08:00
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 : // +
2019-04-13 16:01:51 -04:00
return parseSimpleString ( parser )
2016-11-29 18:29:50 -08:00
case 42 : // *
return parseArray ( parser )
case 45 : // -
return parseError ( parser )
default :
2019-04-13 16:01:51 -04:00
return handleError ( parser , new ParserError (
'Protocol error, got ' + JSON . stringify ( String . fromCharCode ( type ) ) + ' as reply type byte' ,
JSON . stringify ( parser . buffer ) ,
parser . offset
) )
2016-11-29 18:29:50 -08:00
}
}
// 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 ) {
2019-04-13 16:01:51 -04:00
// eslint-disable-next-line valid-typeof
2016-11-29 18:29:50 -08:00
if ( optionTypes . hasOwnProperty ( key ) && typeof options [ key ] !== optionTypes [ key ] ) {
2019-04-13 16:01:51 -04:00
throw new TypeError ( 'The options argument contains the property "' + key + '" that is either unknown or of a wrong type' )
2016-11-29 18:29:50 -08:00
}
}
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'
2019-04-13 16:01:51 -04:00
this . reset ( )
}
/ * *
* Reset the parser values to the initial state
*
* @ returns { undefined }
* /
JavascriptRedisParser . prototype . reset = function ( ) {
2016-11-29 18:29:50 -08:00
this . offset = 0
this . buffer = null
this . bigStrSize = 0
this . bigOffset = 0
this . totalChunkSize = 0
this . bufferCache = [ ]
2019-04-13 16:01:51 -04:00
this . arrayCache = [ ]
this . arrayPos = [ ]
2016-11-29 18:29:50 -08:00
}
/ * *
2019-04-13 16:01:51 -04:00
* Set the returnBuffers option
2016-11-29 18:29:50 -08:00
*
2019-04-13 16:01:51 -04:00
* @ param returnBuffers
* @ returns { undefined }
2016-11-29 18:29:50 -08:00
* /
2019-04-13 16:01:51 -04:00
JavascriptRedisParser . prototype . setReturnBuffers = function ( returnBuffers ) {
if ( typeof returnBuffers !== 'boolean' ) {
throw new TypeError ( 'The returnBuffers argument has to be a boolean' )
2016-11-29 18:29:50 -08:00
}
2019-04-13 16:01:51 -04:00
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' )
2016-11-29 18:29:50 -08:00
}
2019-04-13 16:01:51 -04:00
this . optionStringNumbers = stringNumbers
2016-11-29 18:29:50 -08:00
}
/ * *
* 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
}
}
/ * *
2019-04-13 16:01:51 -04:00
* Check if the requested size fits in the current bufferPool .
* If it does not , reset and increase the bufferPool accordingly .
*
2016-11-29 18:29:50 -08:00
* @ param length
2019-04-13 16:01:51 -04:00
* @ returns { undefined }
2016-11-29 18:29:50 -08:00
* /
2019-04-13 16:01:51 -04:00
function resizeBuffer ( length ) {
2016-11-29 18:29:50 -08:00
if ( bufferPool . length < length + bufferOffset ) {
var multiplier = length > 1024 * 1024 * 75 ? 2 : 3
2019-04-13 16:01:51 -04:00
if ( bufferOffset > 1024 * 1024 * 111 ) {
2016-11-29 18:29:50 -08:00
bufferOffset = 1024 * 1024 * 50
}
2019-04-13 16:01:51 -04:00
bufferPool = bufferAlloc ( length * multiplier + bufferOffset )
2016-11-29 18:29:50 -08:00
bufferOffset = 0
counter ++
if ( interval === null ) {
interval = setInterval ( decreaseBufferPool , 50 )
}
}
2019-04-13 16:01:51 -04:00
}
/ * *
* 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
2016-11-29 18:29:50 -08:00
}
2019-04-13 16:01:51 -04:00
list [ i ] . copy ( bufferPool , bufferOffset , 0 , offset - 2 )
bufferOffset += offset - 2
return bufferPool . slice ( start , bufferOffset )
2016-11-29 18:29:50 -08:00
}
/ * *
* 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
2019-04-13 16:01:51 -04:00
var newBuffer = bufferAlloc ( remainingLength + buffer . length )
2016-11-29 18:29:50 -08:00
this . buffer . copy ( newBuffer , 0 , this . offset , oldLength )
buffer . copy ( newBuffer , remainingLength , 0 , buffer . length )
this . buffer = newBuffer
this . offset = 0
2019-04-13 16:01:51 -04:00
if ( this . arrayCache . length ) {
var arr = parseArrayChunks ( this )
2016-11-29 18:29:50 -08:00
if ( ! arr ) {
return
}
this . returnReply ( arr )
}
} else if ( this . totalChunkSize + buffer . length >= this . bigStrSize ) {
this . bufferCache . push ( buffer )
2019-04-13 16:01:51 -04:00
var tmp = this . optionReturnBuffers ? concatBulkBuffer ( this ) : concatBulkString ( this )
2016-11-29 18:29:50 -08:00
this . bigStrSize = 0
this . bufferCache = [ ]
2019-04-13 16:01:51 -04:00
this . buffer = buffer
if ( this . arrayCache . length ) {
this . arrayCache [ 0 ] [ this . arrayPos [ 0 ] ++ ] = tmp
tmp = parseArrayChunks ( this )
if ( ! tmp ) {
return
}
}
this . returnReply ( tmp )
2016-11-29 18:29:50 -08:00
} 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 ) {
2019-04-13 16:01:51 -04:00
if ( ! this . arrayCache . length ) {
2016-11-29 18:29:50 -08:00
this . offset = offset
}
return
}
if ( type === 45 ) {
this . returnError ( response )
} else {
this . returnReply ( response )
}
}
this . buffer = null
}
module . exports = JavascriptRedisParser