mirror of
				https://codeberg.org/tacerus/teddit.git
				synced 2025-11-04 05:27:20 +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
 |