diff --git a/.gitignore b/.gitignore index 594729d..cd32cd1 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ logs *.log coverage node_modules +.idea diff --git a/.jshintignore b/.jshintignore deleted file mode 100644 index ed454a5..0000000 --- a/.jshintignore +++ /dev/null @@ -1,4 +0,0 @@ -node_modules/** -coverage/** -**.md -**.log \ No newline at end of file diff --git a/.jshintrc b/.jshintrc deleted file mode 100644 index 986423d..0000000 --- a/.jshintrc +++ /dev/null @@ -1,20 +0,0 @@ -{ - "eqeqeq": true, // Prohibits the use of == and != in favor of === and !== - "noarg": true, // Prohibit use of `arguments.caller` and `arguments.callee` - "undef": true, // Require all non-global variables be declared before they are used. - "unused": "vars", // Warn unused variables, but not unused params - "strict": true, // Require `use strict` pragma in every file. - "nonbsp": true, // don't allow non utf-8 pages to break - "forin": true, // don't allow not filtert for in loops - "freeze": true, // prohibit overwriting prototypes of native objects - "maxdepth": 4, - "latedef": true, - "maxparams": 3, - - // Environment options - "node": true, // Enable globals available when code is running inside of the NodeJS runtime environment. - "mocha": true, - - // Relaxing options - "boss": true // Accept statements like `while (key = keys.pop()) {}` -} diff --git a/.npmignore b/.npmignore new file mode 100644 index 0000000..1c22fdc --- /dev/null +++ b/.npmignore @@ -0,0 +1,12 @@ +# IntelliJ project files +.idea +*.iml +out +gen + +# Unrelevant files and folders +benchmark +coverage +test +.travis.yml +.gitignore diff --git a/.travis.yml b/.travis.yml index a7c67a9..e412156 100644 --- a/.travis.yml +++ b/.travis.yml @@ -12,7 +12,7 @@ node_js: - "0.10" - "0.12" - "4" - - "5" + - "6" install: - npm install - npm install hiredis diff --git a/README.md b/README.md index 6a5df8c..6843dc3 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,11 @@ [![Build Status](https://travis-ci.org/NodeRedis/node-redis-parser.png?branch=master)](https://travis-ci.org/NodeRedis/node-redis-parser) [![Code Climate](https://codeclimate.com/github/NodeRedis/node-redis-parser/badges/gpa.svg)](https://codeclimate.com/github/NodeRedis/node-redis-parser) [![Test Coverage](https://codeclimate.com/github/NodeRedis/node-redis-parser/badges/coverage.svg)](https://codeclimate.com/github/NodeRedis/node-redis-parser/coverage) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg)](http://standardjs.com/) # redis-parser -A high performance redis parser solution built for [node_redis](https://github.com/NodeRedis/node_redis) and [ioredis](https://github.com/ioredis/luin). - -Generally all [RESP](http://redis.io/topics/protocol) data will be properly parsed by the parser. +A high performance javascript redis parser built for [node_redis](https://github.com/NodeRedis/node_redis) and [ioredis](https://github.com/luin/ioredis). Parses all [RESP](http://redis.io/topics/protocol) data. ## Install @@ -21,7 +20,7 @@ npm install redis-parser ```js var Parser = require('redis-parser'); -new Parser(options); +var myParser = new Parser(options); ``` ### Possible options @@ -30,8 +29,6 @@ new Parser(options); * `returnError`: *function*; mandatory * `returnFatalError`: *function*; optional, defaults to the returnError function * `returnBuffers`: *boolean*; optional, defaults to false -* `name`: *'javascript'|'hiredis'|'auto'|null*; optional, defaults to hiredis and falls back to the js parser if not available or if the stringNumbers option is choosen. Setting this to 'auto' or null is going to automatically determine what parser is available and chooses that one. -* `stringNumbers`: *boolean*; optional, defaults to false. This is only available for the javascript parser at the moment! ### Example @@ -55,8 +52,7 @@ var parser = new Parser({ }, returnFatalError: function (err) { lib.returnFatalError(err); - }, - name: 'auto' // This returns either the hiredis or the js parser instance depending on what's available + } }); Library.prototype.streamHandler = function () { @@ -70,7 +66,7 @@ You do not have to use the returnFatalError function. Fatal errors will be retur And if you want to return buffers instead of strings, you can do this by adding the `returnBuffers` option. -If you handle big numbers, you should pass the `stringNumbers` option. That case numbers above 2^53 can be handled properly without reduced precision. +Big numbers that are too large for JS are automatically stringified. ```js // Same functions as in the first example @@ -82,44 +78,24 @@ var parser = new Parser({ returnError: function(err) { lib.returnError(err); }, - name: 'javascript', // Use the Javascript parser - stringNumbers: true, // Return all numbers as string instead of a js number returnBuffers: true // All strings are returned as buffer e.g. }); // The streamHandler as above ``` -## Further info - -The [hiredis](https://github.com/redis/hiredis) parser is still the fasted parser for -Node.js and therefor used as default in redis-parser if the hiredis parser is available. - -Otherwise the pure js NodeRedis parser is choosen that is almost as fast as the -hiredis parser besides some situations in which it'll be a bit slower. - ## Protocol errors -To handle protocol errors (this is very unlikely to happen) gracefuly you should add the returnFatalError option, reject any still running command (they might have been processed properly but the reply is just wrong), destroy the socket and reconnect. -Otherwise a chunk might still contain partial data of a following command that was already processed properly but answered in the same chunk as the command that resulted in the protocol error. +To handle protocol errors (this is very unlikely to happen) gracefully you should add the returnFatalError option, reject any still running command (they might have been processed properly but the reply is just wrong), destroy the socket and reconnect. Note that while doing this no new command may be added, so all new commands have to be buffered in the meantime, otherwise a chunk might still contain partial data of a following command that was already processed properly but answered in the same chunk as the command that resulted in the protocol error. ## Contribute -The js parser is already optimized but there are likely further optimizations possible. -Besides running the tests you'll also have to run the change at least against the node_redis benchmark suite and post the improvement in the PR. -If you want to write a own parser benchmark, that would also be great! +The parser is highly optimized but there may still be further optimizations possible. ``` npm install npm test - -# Run node_redis benchmark (let's guess you cloned node_redis in another folder) -cd ../redis -npm install -npm run benchmark parser=javascript > old.log -# Replace the changed parser in the node_modules -npm run benchmark parser=javascript > new.log -node benchmarks/diff_multi_bench_output.js old.log new.log > improvement.log +npm run benchmark ``` ## License diff --git a/benchmark/index.js b/benchmark/index.js new file mode 100644 index 0000000..a2ba0ff --- /dev/null +++ b/benchmark/index.js @@ -0,0 +1,301 @@ +var Benchmark = require('benchmark') +var suite = new Benchmark.Suite() + +var Parser = require('./../') +var ParserOLD = require('./old/parser') + +function returnError (error) { + error = null +} + +function checkReply () {} + +function shuffle (array) { + var currentIndex = array.length + var temporaryValue + var randomIndex + + // While there remain elements to shuffle... + while (currentIndex !== 0) { + // Pick a remaining element... + randomIndex = Math.floor(Math.random() * currentIndex) + currentIndex -= 1 + + // And swap it with the current element. + temporaryValue = array[currentIndex] + array[currentIndex] = array[randomIndex] + array[randomIndex] = temporaryValue + } + + return array +} + +var startBuffer = new Buffer('$100\r\nabcdefghij') +var chunkBuffer = new Buffer('abcdefghijabcdefghijabcdefghij') +var stringBuffer = new Buffer('+testing a simple string\r\n') +var integerBuffer = new Buffer(':1237884\r\n') +var bigIntegerBuffer = new Buffer(':18446744073709551617\r\n') // 2^64 + 1 +var errorBuffer = new Buffer('-Error ohnoesitbroke\r\n') +var arrayBuffer = new Buffer('*1\r\n*1\r\n$1\r\na\r\n') +var endBuffer = new Buffer('\r\n') +var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + + 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars +var bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long +var startBigBuffer = new Buffer('$' + (4 * 1024 * 1024) + '\r\n') +var chunks = new Array(64) +for (var i = 0; i < 64; i++) { + chunks[i] = new Buffer(shuffle(bigStringArray).join(' ') + '.') // Math.pow(2, 16) chars long +} + +var bigArraySize = 100 +var bigArray = '*' + bigArraySize + '\r\n' +for (i = 0; i < bigArraySize; i++) { + bigArray += '$' + var size = (Math.random() * 10 | 0) + 1 + bigArray += size + '\r\n' + lorem.slice(0, size) + '\r\n' +} + +var bigArrayBuffer = new Buffer(bigArray) +var chunkedStringPart1 = new Buffer('+foobar') +var chunkedStringPart2 = new Buffer('bazEND\r\n') + +var parserOld = new ParserOLD({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnError, + name: 'javascript' +}) + +var parserHiRedis = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnError, + name: 'hiredis' +}) + +var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnError +}) + +var parserBuffer = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnError, + returnBuffers: true +}) + +var parserStr = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnError, + stringNumbers: true +}) + +// BULK STRINGS + +suite.add('OLD CODE: multiple chunks in a bulk string', function () { + parserOld.execute(startBuffer) + parserOld.execute(chunkBuffer) + parserOld.execute(chunkBuffer) + parserOld.execute(chunkBuffer) + parserOld.execute(endBuffer) +}) + +suite.add('HIREDIS: multiple chunks in a bulk string', function () { + parserHiRedis.execute(startBuffer) + parserHiRedis.execute(chunkBuffer) + parserHiRedis.execute(chunkBuffer) + parserHiRedis.execute(chunkBuffer) + parserHiRedis.execute(endBuffer) +}) + +suite.add('NEW CODE: multiple chunks in a bulk string', function () { + parser.execute(startBuffer) + parser.execute(chunkBuffer) + parser.execute(chunkBuffer) + parser.execute(chunkBuffer) + parser.execute(endBuffer) +}) + +suite.add('NEW BUF: multiple chunks in a bulk string', function () { + parserBuffer.execute(startBuffer) + parserBuffer.execute(chunkBuffer) + parserBuffer.execute(chunkBuffer) + parserBuffer.execute(chunkBuffer) + parserBuffer.execute(endBuffer) +}) + +// CHUNKED STRINGS + +suite.add('\nOLD CODE: multiple chunks in a string', function () { + parserOld.execute(chunkedStringPart1) + parserOld.execute(chunkedStringPart2) +}) + +suite.add('HIREDIS: multiple chunks in a string', function () { + parserHiRedis.execute(chunkedStringPart1) + parserHiRedis.execute(chunkedStringPart2) +}) + +suite.add('NEW CODE: multiple chunks in a string', function () { + parser.execute(chunkedStringPart1) + parser.execute(chunkedStringPart2) +}) + +suite.add('NEW BUF: multiple chunks in a string', function () { + parserBuffer.execute(chunkedStringPart1) + parserBuffer.execute(chunkedStringPart2) +}) + +// BIG BULK STRING + +suite.add('\nOLD CODE: 4mb bulk string', function () { + parserOld.execute(startBigBuffer) + for (var i = 0; i < 64; i++) { + parserOld.execute(chunks[i]) + } + parserOld.execute(endBuffer) +}) + +suite.add('HIREDIS: 4mb bulk string', function () { + parserHiRedis.execute(startBigBuffer) + for (var i = 0; i < 64; i++) { + parserHiRedis.execute(chunks[i]) + } + parserHiRedis.execute(endBuffer) +}) + +suite.add('NEW CODE: 4mb bulk string', function () { + parser.execute(startBigBuffer) + for (var i = 0; i < 64; i++) { + parser.execute(chunks[i]) + } + parser.execute(endBuffer) +}) + +suite.add('NEW BUF: 4mb bulk string', function () { + parserBuffer.execute(startBigBuffer) + for (var i = 0; i < 64; i++) { + parserBuffer.execute(chunks[i]) + } + parserBuffer.execute(endBuffer) +}) + +// STRINGS + +suite.add('\nOLD CODE: + simple string', function () { + parserOld.execute(stringBuffer) +}) + +suite.add('HIREDIS: + simple string', function () { + parserHiRedis.execute(stringBuffer) +}) + +suite.add('NEW CODE: + simple string', function () { + parser.execute(stringBuffer) +}) + +suite.add('NEW BUF: + simple string', function () { + parserBuffer.execute(stringBuffer) +}) + +// INTEGERS + +suite.add('\nOLD CODE: + integer', function () { + parserOld.execute(integerBuffer) +}) + +suite.add('HIREDIS: + integer', function () { + parserHiRedis.execute(integerBuffer) +}) + +suite.add('NEW CODE: + integer', function () { + parser.execute(integerBuffer) +}) + +suite.add('NEW STR: + integer', function () { + parserStr.execute(integerBuffer) +}) + +// BIG INTEGER + +suite.add('\nOLD CODE: + big integer', function () { + parserOld.execute(bigIntegerBuffer) +}) + +suite.add('HIREDIS: + big integer', function () { + parserHiRedis.execute(bigIntegerBuffer) +}) + +suite.add('NEW CODE: + big integer', function () { + parser.execute(bigIntegerBuffer) +}) + +suite.add('NEW STR: + big integer', function () { + parserStr.execute(bigIntegerBuffer) +}) + +// ARRAYS + +suite.add('\nOLD CODE: * array', function () { + parserOld.execute(arrayBuffer) +}) + +suite.add('HIREDIS: * array', function () { + parserHiRedis.execute(arrayBuffer) +}) + +suite.add('NEW CODE: * array', function () { + parser.execute(arrayBuffer) +}) + +suite.add('NEW BUF: * array', function () { + parserBuffer.execute(arrayBuffer) +}) + +// BIG ARRAYS + +suite.add('\nOLD CODE: * bigArray', function () { + parserOld.execute(bigArrayBuffer) +}) + +suite.add('HIREDIS: * bigArray', function () { + parserHiRedis.execute(bigArrayBuffer) +}) + +suite.add('NEW CODE: * bigArray', function () { + parser.execute(bigArrayBuffer) +}) + +suite.add('NEW BUF: * bigArray', function () { + parserBuffer.execute(bigArrayBuffer) +}) + +// ERRORS + +suite.add('\nOLD CODE: * error', function () { + parserOld.execute(errorBuffer) +}) + +suite.add('HIREDIS: * error', function () { + parserHiRedis.execute(errorBuffer) +}) + +suite.add('NEW CODE: * error', function () { + parser.execute(errorBuffer) +}) + +// add listeners +suite.on('cycle', function (event) { + console.log(String(event.target)) +}) + +suite.on('complete', function () { + console.log('\n\nFastest is ' + this.filter('fastest').map('name')) +}) + +suite.run({ delay: 1, minSamples: 150 }) diff --git a/benchmark/old/hiredis.js b/benchmark/old/hiredis.js new file mode 100644 index 0000000..9e3fe45 --- /dev/null +++ b/benchmark/old/hiredis.js @@ -0,0 +1,36 @@ +'use strict' + +var hiredis = require('hiredis') + +function HiredisReplyParser (options) { + this.name = 'hiredis' + this.options = options + this.reader = new hiredis.Reader(options) +} + +HiredisReplyParser.prototype.parseData = function () { + try { + return this.reader.get() + } catch (err) { + // Protocol errors land here + // Reset the parser. Otherwise new commands can't be processed properly + this.reader = new hiredis.Reader(this.options) + this.returnFatalError(err) + } +} + +HiredisReplyParser.prototype.execute = function (data) { + this.reader.feed(data) + var reply = this.parseData() + + while (reply !== undefined) { + if (reply && reply.name === 'Error') { + this.returnError(reply) + } else { + this.returnReply(reply) + } + reply = this.parseData() + } +} + +module.exports = HiredisReplyParser diff --git a/benchmark/old/javascript.js b/benchmark/old/javascript.js new file mode 100644 index 0000000..d742e98 --- /dev/null +++ b/benchmark/old/javascript.js @@ -0,0 +1,172 @@ +'use strict' + +function JavascriptReplyParser (options) { + this.name = 'javascript_old' + this.buffer = new Buffer(0) + this.offset = 0 + this.bigStrSize = 0 + this.chunksSize = 0 + this.buffers = [] + this.type = 0 + this.protocolError = false + this.offsetCache = 0 + // If returnBuffers is active, all return values are returned as buffers besides numbers and errors + if (options.return_buffers) { + this.handleReply = function (start, end) { + return this.buffer.slice(start, end) + } + } else { + this.handleReply = function (start, end) { + return this.buffer.toString('utf-8', start, end) + } + } + // 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 + if (options.string_numbers) { + this.handleNumbers = function (start, end) { + return this.buffer.toString('ascii', start, end) + } + } else { + this.handleNumbers = function (start, end) { + return +this.buffer.toString('ascii', start, end) + } + } +} + +JavascriptReplyParser.prototype.parseResult = function (type) { + var start = 0 + var end = 0 + var packetHeader = 0 + var reply + + if (type === 36) { // $ + packetHeader = this.parseHeader() + // Packets with a size of -1 are considered null + if (packetHeader === -1) { + return null + } + end = this.offset + packetHeader + start = this.offset + if (end + 2 > this.buffer.length) { + this.buffers.push(this.offsetCache === 0 ? this.buffer : this.buffer.slice(this.offsetCache)) + this.chunksSize = this.buffers[0].length + // Include the packetHeader delimiter + this.bigStrSize = packetHeader + 2 + throw new Error('Wait for more data.') + } + // Set the offset to after the delimiter + this.offset = end + 2 + return this.handleReply(start, end) + } else if (type === 58) { // : + // Up to the delimiter + end = this.packetEndOffset() + start = this.offset + // Include the delimiter + this.offset = end + 2 + // Return the coerced numeric value + return this.handleNumbers(start, end) + } else if (type === 43) { // + + end = this.packetEndOffset() + start = this.offset + this.offset = end + 2 + return this.handleReply(start, end) + } else if (type === 42) { // * + packetHeader = this.parseHeader() + if (packetHeader === -1) { + return null + } + reply = [] + for (var i = 0; i < packetHeader; i++) { + if (this.offset >= this.buffer.length) { + throw new Error('Wait for more data.') + } + reply.push(this.parseResult(this.buffer[this.offset++])) + } + return reply + } else if (type === 45) { // - + end = this.packetEndOffset() + start = this.offset + this.offset = end + 2 + return new Error(this.buffer.toString('utf-8', start, end)) + } +} + +JavascriptReplyParser.prototype.execute = function (buffer) { + if (this.chunksSize !== 0) { + if (this.bigStrSize > this.chunksSize + buffer.length) { + this.buffers.push(buffer) + this.chunksSize += buffer.length + return + } + this.buffers.push(buffer) + this.buffer = Buffer.concat(this.buffers, this.chunksSize + buffer.length) + this.buffers = [] + this.bigStrSize = 0 + this.chunksSize = 0 + } else if (this.offset >= this.buffer.length) { + this.buffer = buffer + } else { + this.buffer = Buffer.concat([this.buffer.slice(this.offset), buffer]) + } + this.offset = 0 + this.run() +} + +JavascriptReplyParser.prototype.tryParsing = function () { + try { + return this.parseResult(this.type) + } catch (err) { + // Catch the error (not enough data), rewind if it's an array, + // and wait for the next packet to appear + this.offset = this.offsetCache + // Indicate that there's no protocol error by resetting the type too + this.type = undefined + } +} + +JavascriptReplyParser.prototype.run = function () { + // Set a rewind point. If a failure occurs, wait for the next execute()/append() and try again + this.offsetCache = this.offset + this.type = this.buffer[this.offset++] + var reply = this.tryParsing() + + while (reply !== undefined) { + if (this.type === 45) { // Errors - + this.returnError(reply) + } else { + this.returnReply(reply) // Strings + // Integers : // Bulk strings $ // Arrays * + } + this.offsetCache = this.offset + this.type = this.buffer[this.offset++] + reply = this.tryParsing() + } + if (this.type !== undefined) { + // Reset the buffer so the parser can handle following commands properly + this.buffer = new Buffer(0) + this.returnFatalError(new Error('Protocol error, got ' + JSON.stringify(String.fromCharCode(this.type)) + ' as reply type byte')) + } +} + +JavascriptReplyParser.prototype.parseHeader = function () { + var end = this.packetEndOffset() + var value = this.buffer.toString('ascii', this.offset, end) | 0 + + this.offset = end + 2 + return value +} + +JavascriptReplyParser.prototype.packetEndOffset = function () { + var offset = this.offset + var len = this.buffer.length - 1 + + while (this.buffer[offset] !== 0x0d && this.buffer[offset + 1] !== 0x0a) { + offset++ + + if (offset >= len) { + throw new Error('Did not see LF after NL reading multi bulk count (' + offset + ' => ' + this.buffer.length + ', ' + this.offset + ')') + } + } + return offset +} + +module.exports = JavascriptReplyParser diff --git a/benchmark/old/parser.js b/benchmark/old/parser.js new file mode 100644 index 0000000..e8ca306 --- /dev/null +++ b/benchmark/old/parser.js @@ -0,0 +1,59 @@ +'use strict' + +var parsers = { + javascript: require('./javascript') +} + +// Hiredis might not be installed +try { + parsers.hiredis = require('./hiredis') +} catch (err) { /* ignore errors */ } + +function Parser (options) { + var parser, msg + + if ( + !options || + typeof options.returnError !== 'function' || + typeof options.returnReply !== 'function' + ) { + throw new Error('Please provide all return functions while initiating the parser') + } + + if (options.name === 'hiredis') { + /* istanbul ignore if: hiredis should always be installed while testing */ + if (!parsers.hiredis) { + msg = 'You explicitly required the hiredis parser but hiredis is not installed. The JS parser is going to be returned instead.' + } else if (options.stringNumbers) { + msg = 'You explicitly required the hiredis parser in combination with the stringNumbers option. Only the JS parser can handle that and is choosen instead.' + } + } else if (options.name && !parsers[options.name] && options.name !== 'auto') { + msg = 'The requested parser "' + options.name + '" is unkown and the default parser is choosen instead.' + } + + if (msg) { + console.warn(new Error(msg).stack.replace('Error: ', 'Warning: ')) + } + + options.name = options.name || 'hiredis' + options.name = options.name.toLowerCase() + + var innerOptions = { + // The hiredis parser expects underscores + return_buffers: options.returnBuffers || false, + string_numbers: options.stringNumbers || false + } + + if (options.name === 'javascript' || !parsers.hiredis || options.stringNumbers) { + parser = new parsers.javascript(innerOptions) // eslint-disable-line new-cap + } else { + parser = new parsers.hiredis(innerOptions) // eslint-disable-line new-cap + } + + parser.returnError = options.returnError + parser.returnFatalError = options.returnFatalError || options.returnError + parser.returnReply = options.returnReply + return parser +} + +module.exports = Parser diff --git a/changelog.md b/changelog.md index 4efb816..82f4521 100644 --- a/changelog.md +++ b/changelog.md @@ -1,3 +1,22 @@ +## v.2.0.0 - 0x May, 2016 + +The javascript parser got completly rewritten by [Michael Diarmid](https://github.com/Salakar) and [Ruben Bridgewater](https://github.com/BridgeAR) and is now a lot faster than the hiredis parser. +Therefore the hiredis parser was deprecated and should only be used for testing purposes and benchmarking comparison. + +All Errors returned by the parser are from now on of class ReplyError + +Features + +- Improved performance by up to 15x as fast as before +- Improved options validation +- Added ReplyError Class +- Added parser benchmark +- Switched default parser from hiredis to JS, no matter if hiredis is installed or not + +Removed + +- Deprecated hiredis support + ## v.1.3.0 - 27 Mar, 2016 Features diff --git a/index.js b/index.js index a12aaf2..918a9fe 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,4 @@ -'use strict'; +'use strict' -module.exports = require('./lib/parser'); +module.exports = require('./lib/parser') +module.exports.ReplyError = require('./lib/replyError') diff --git a/lib/hiredis.js b/lib/hiredis.js index 965b660..535c3ef 100644 --- a/lib/hiredis.js +++ b/lib/hiredis.js @@ -1,36 +1,52 @@ -'use strict'; +'use strict' -var hiredis = require('hiredis'); +var hiredis = require('hiredis') +var ReplyError = require('../lib/replyError') -function HiredisReplyParser(options) { - this.name = 'hiredis'; - this.options = options; - this.reader = new hiredis.Reader(options); +/** + * Parse data + * @param parser + * @returns {*} + */ +function parseData (parser) { + try { + return parser.reader.get() + } catch (err) { + // Protocol errors land here + // Reset the parser. Otherwise new commands can't be processed properly + parser.reader = new hiredis.Reader(parser.options) + parser.returnFatalError(new ReplyError(err.message)) + } } -HiredisReplyParser.prototype.parseData = function () { - try { - return this.reader.get(); - } catch (err) { - // Protocol errors land here - // Reset the parser. Otherwise new commands can't be processed properly - this.reader = new hiredis.Reader(this.options); - this.returnFatalError(err); - } -}; +/** + * Hiredis Parser + * @param options + * @constructor + */ +function HiredisReplyParser (options) { + this.returnError = options.returnError + this.returnFatalError = options.returnFatalError || options.returnError + this.returnReply = options.returnReply + this.name = 'hiredis' + this.options = { + return_buffers: !!options.returnBuffers + } + this.reader = new hiredis.Reader(this.options) +} HiredisReplyParser.prototype.execute = function (data) { - this.reader.feed(data); - var reply = this.parseData(); + this.reader.feed(data) + var reply = parseData(this) - while (reply !== undefined) { - if (reply && reply.name === 'Error') { - this.returnError(reply); - } else { - this.returnReply(reply); - } - reply = this.parseData(); + while (reply !== undefined) { + if (reply && reply.name === 'Error') { + this.returnError(new ReplyError(reply.message)) + } else { + this.returnReply(reply) } -}; + reply = parseData(this) + } +} -module.exports = HiredisReplyParser; +module.exports = HiredisReplyParser diff --git a/lib/javascript.js b/lib/javascript.js deleted file mode 100644 index a110345..0000000 --- a/lib/javascript.js +++ /dev/null @@ -1,172 +0,0 @@ -'use strict'; - -function JavascriptReplyParser(options) { - this.name = 'javascript'; - this.buffer = new Buffer(0); - this.offset = 0; - this.bigStrSize = 0; - this.chunksSize = 0; - this.buffers = []; - this.type = 0; - this.protocolError = false; - this.offsetCache = 0; - // If returnBuffers is active, all return values are returned as buffers besides numbers and errors - if (options.return_buffers) { - this.handleReply = function (start, end) { - return this.buffer.slice(start, end); - }; - } else { - this.handleReply = function (start, end) { - return this.buffer.toString('utf-8', start, end); - }; - } - // 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 - if (options.string_numbers) { - this.handleNumbers = function (start, end) { - return this.buffer.toString('ascii', start, end); - }; - } else { - this.handleNumbers = function (start, end) { - return +this.buffer.toString('ascii', start, end); - }; - } -} - -JavascriptReplyParser.prototype.parseResult = function (type) { - var start = 0, - end = 0, - packetHeader = 0, - reply; - - if (type === 36) { // $ - packetHeader = this.parseHeader(); - // Packets with a size of -1 are considered null - if (packetHeader === -1) { - return null; - } - end = this.offset + packetHeader; - start = this.offset; - if (end + 2 > this.buffer.length) { - this.buffers.push(this.offsetCache === 0 ? this.buffer : this.buffer.slice(this.offsetCache)); - this.chunksSize = this.buffers[0].length; - // Include the packetHeader delimiter - this.bigStrSize = packetHeader + 2; - throw new Error('Wait for more data.'); - } - // Set the offset to after the delimiter - this.offset = end + 2; - return this.handleReply(start, end); - } else if (type === 58) { // : - // Up to the delimiter - end = this.packetEndOffset(); - start = this.offset; - // Include the delimiter - this.offset = end + 2; - // Return the coerced numeric value - return this.handleNumbers(start, end); - } else if (type === 43) { // + - end = this.packetEndOffset(); - start = this.offset; - this.offset = end + 2; - return this.handleReply(start, end); - } else if (type === 42) { // * - packetHeader = this.parseHeader(); - if (packetHeader === -1) { - return null; - } - reply = []; - for (var i = 0; i < packetHeader; i++) { - if (this.offset >= this.buffer.length) { - throw new Error('Wait for more data.'); - } - reply.push(this.parseResult(this.buffer[this.offset++])); - } - return reply; - } else if (type === 45) { // - - end = this.packetEndOffset(); - start = this.offset; - this.offset = end + 2; - return new Error(this.buffer.toString('utf-8', start, end)); - } -}; - -JavascriptReplyParser.prototype.execute = function (buffer) { - if (this.chunksSize !== 0) { - if (this.bigStrSize > this.chunksSize + buffer.length) { - this.buffers.push(buffer); - this.chunksSize += buffer.length; - return; - } - this.buffers.push(buffer); - this.buffer = Buffer.concat(this.buffers, this.chunksSize + buffer.length); - this.buffers = []; - this.bigStrSize = 0; - this.chunksSize = 0; - } else if (this.offset >= this.buffer.length) { - this.buffer = buffer; - } else { - this.buffer = Buffer.concat([this.buffer.slice(this.offset), buffer]); - } - this.offset = 0; - this.run(); -}; - -JavascriptReplyParser.prototype.tryParsing = function () { - try { - return this.parseResult(this.type); - } catch (err) { - // Catch the error (not enough data), rewind if it's an array, - // and wait for the next packet to appear - this.offset = this.offsetCache; - // Indicate that there's no protocol error by resetting the type too - this.type = undefined; - } -}; - -JavascriptReplyParser.prototype.run = function () { - // Set a rewind point. If a failure occurs, wait for the next execute()/append() and try again - this.offsetCache = this.offset; - this.type = this.buffer[this.offset++]; - var reply = this.tryParsing(); - - while (reply !== undefined) { - if (this.type === 45) { // Errors - - this.returnError(reply); - } else { - this.returnReply(reply); // Strings + // Integers : // Bulk strings $ // Arrays * - } - this.offsetCache = this.offset; - this.type = this.buffer[this.offset++]; - reply = this.tryParsing(); - } - if (this.type !== undefined) { - // Reset the buffer so the parser can handle following commands properly - this.buffer = new Buffer(0); - this.returnFatalError(new Error('Protocol error, got ' + JSON.stringify(String.fromCharCode(this.type)) + ' as reply type byte')); - } -}; - -JavascriptReplyParser.prototype.parseHeader = function () { - var end = this.packetEndOffset(), - value = this.buffer.toString('ascii', this.offset, end) | 0; - - this.offset = end + 2; - return value; -}; - -JavascriptReplyParser.prototype.packetEndOffset = function () { - var offset = this.offset, - len = this.buffer.length - 1; - - while (this.buffer[offset] !== 0x0d && this.buffer[offset + 1] !== 0x0a) { - offset++; - - if (offset >= len) { - throw new Error('Did not see LF after NL reading multi bulk count (' + offset + ' => ' + this.buffer.length + ', ' + this.offset + ')'); - } - } - return offset; -}; - -module.exports = JavascriptReplyParser; diff --git a/lib/parser.js b/lib/parser.js index 2180652..8ae4df5 100644 --- a/lib/parser.js +++ b/lib/parser.js @@ -1,59 +1,399 @@ -'use strict'; +'use strict' -var parsers = { - javascript: require('./javascript') -}; +var ReplyError = require('./replyError') +var bufferPool = new Buffer(64 * 1024) +var interval = null -// Hiredis might not be installed -try { - parsers.hiredis = require('./hiredis'); -} catch (err) { /* ignore errors */ } +/** + * 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 + var number = 0 + var sign = false -function Parser (options) { - var parser, msg; + if (parser.buffer[offset] === 45) { + sign = true + offset++ + } - if ( - !options || - typeof options.returnError !== 'function' || - typeof options.returnReply !== 'function' - ) { - throw new Error('Please provide all return functions while initiating the parser'); + while (offset < length) { + var c1 = parser.buffer[offset++] + if (c1 === 13 && parser.buffer[offset] === 10) { // \r\n + parser.offset = offset + 1 + return sign ? -number : number } + number = (number * 10) + (c1 - 48) + } +} + +/** + * Used for integer numbers in case of the returnNumbers option + * @param parser + * @returns {*} + */ +function parseStringNumbers (parser) { + var offset = parser.offset + var length = parser.buffer.length + var number = '' + + if (parser.buffer[offset] === 45) { + number += '-' + offset++ + } + + while (offset < length) { + var c1 = parser.buffer[offset++] + if (c1 === 13 && parser.buffer[offset] === 10) { // \r\n + parser.offset = offset + 1 + return number + } + number += c1 - 48 + } +} + +/** + * Returns a string or buffer of the provided offset start and + * end ranges. Checks `optionReturnBuffers`. + * @param parser + * @param start + * @param end + * @returns {*} + */ +function convertBufferRange (parser, start, end) { + // If returnBuffers is active, all return values are returned as buffers besides numbers and errors + 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 parseSimpleStringViaOffset (parser) { + var start = parser.offset + var offset = parser.offset + var length = parser.buffer.length + var buffer = parser.buffer + + while (offset < length) { + if (buffer[offset++] === 10) { // \r\n + return convertBufferRange(parser, start, offset - 2) + } + } +} + +/** + * 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 + * @param parser + * @returns {*} + */ +function parseInteger (parser) { + // 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 + 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 = parseSimpleStringViaOffset(parser) + if (string !== undefined) { + if (parser.optionReturnBuffers === true) { + string = string.toString() + } + return new ReplyError(string) + } +} - if (options.name === 'hiredis') { - /* istanbul ignore if: hiredis should always be installed while testing */ - if (!parsers.hiredis) { - msg = 'You explicitly required the hiredis parser but hiredis is not installed. The JS parser is going to be returned instead.'; - } else if (options.stringNumbers) { - msg = 'You explicitly required the hiredis parser in combination with the stringNumbers option. Only the JS parser can handle that and is choosen instead.'; - } - } else if (options.name && !parsers[options.name] && options.name !== 'auto') { - msg = 'The requested parser "' + options.name + '" is unkown and the default parser is choosen instead.'; +/** + * 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) + var bufferLength = parser.buffer.length + for (var i = 0; i < length; i++) { + if (parser.offset >= bufferLength) { + return } + var response = parseType(parser, parser.buffer[parser.offset++]) + if (response === undefined) { + return + } + responses[i] = response + } + + return responses +} - if (msg) { - console.warn(new Error(msg).stack.replace('Error: ', 'Warning: ')); +/** + * 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 parseSimpleStringViaOffset(parser) + case 42: // * + return parseArray(parser) + case 45: // - + return parseError(parser) + default: + return handleError(parser, new ReplyError('Protocol error, got ' + JSON.stringify(String.fromCharCode(type)) + ' as reply type byte')) + } +} + +// 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) { + if (typeof options[key] !== optionTypes[key]) { + throw new TypeError('The options argument contains the property "' + key + '" that is either unkown 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.offset = 0 + this.buffer = null + this.bigStrSize = 0 + this.bigOffset = 0 + this.totalChunkSize = 0 + this.bufferCache = [] +} - options.name = options.name || 'hiredis'; - options.name = options.name.toLowerCase(); +/** + * Concat a bulk string containing multiple chunks + * @param parser + * @param buffer + * @returns {String} + */ +function concatBulkString (parser) { + var list = parser.bufferCache + // The first chunk might contain the whole bulk string including the \r + var chunks = list.length + var offset = parser.bigStrSize - parser.totalChunkSize + parser.offset = offset + if (offset === 1) { + if (chunks === 2) { + return list[0].toString('utf8', parser.bigOffset, list[0].length - 1) + } + } else { + chunks++ + } + var res = list[0].toString('utf8', parser.bigOffset) + for (var i = 1; i < chunks - 2; i++) { + // We are only safe to fully add up elements that are neither the first nor any of the last two elements + res += list[i].toString() + } + res += list[i].toString('utf8', 0, offset === 1 ? list[i].length - 1 : offset - 2) + return res +} + +/** + * Decrease the bufferPool size over time + * @returns {undefined} + */ +function decreaseBufferPool () { + if (bufferPool.length > 96 * 1024) { + // Decrease the bufferPool by 16kb + bufferPool = bufferPool.slice(0, bufferPool.length - 16 * 1024) + } else { + clearInterval(interval) + interval = null + } +} - var innerOptions = { - // The hiredis parser expects underscores - return_buffers: options.returnBuffers || false, - string_numbers: options.stringNumbers || false - }; +/** + * Concat the collected chunks from parser.bufferCache + * @param parser + * @param length + * @returns {Buffer} + */ +function concatBuffer (parser, length) { + var list = parser.bufferCache + var pos = 0 + if (bufferPool.length < length) { + bufferPool = new Buffer(length) + if (interval === null) { + interval = setInterval(decreaseBufferPool, 50) + } + } + for (var i = 0; i < list.length; i++) { + list[i].copy(bufferPool, pos) + pos += list[i].length + } + return bufferPool.slice(parser.offset, length) +} + +/** + * Parse the redis buffer + * @param buffer + * @returns {undefined} + */ +JavascriptRedisParser.prototype.execute = function (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 newLength = remainingLength + buffer.length + // ~ 5% speed increase over using new Buffer(length) all the time + if (bufferPool.length < newLength) { // We can't rely on the chunk size + bufferPool = new Buffer(newLength) + } + this.buffer.copy(bufferPool, 0, this.offset, oldLength) + buffer.copy(bufferPool, remainingLength, 0, buffer.length) + this.buffer = bufferPool.slice(0, newLength) + this.offset = 0 + } else if (this.totalChunkSize + buffer.length >= this.bigStrSize) { + this.bufferCache.push(buffer) + // The returned type might be Array * (42) and in that case we can't improve the parsing currently + if (this.optionReturnBuffers === false && this.buffer[this.offset] === 36) { + this.returnReply(concatBulkString(this)) + this.buffer = buffer + } else { // This applies for arrays too + this.buffer = concatBuffer(this, this.totalChunkSize + buffer.length) + this.offset = 0 + } + this.bigStrSize = 0 + this.totalChunkSize = 0 + this.bufferCache = [] + } 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) { + this.offset = offset + return + } - if (options.name === 'javascript' || !parsers.hiredis || options.stringNumbers) { - parser = new parsers.javascript(innerOptions); + if (type === 45) { + this.returnError(response) // Errors - } else { - parser = new parsers.hiredis(innerOptions); + this.returnReply(response) // Strings + // Integers : // Bulk strings $ // Arrays * } + } - parser.returnError = options.returnError; - parser.returnFatalError = options.returnFatalError || options.returnError; - parser.returnReply = options.returnReply; - return parser; + this.buffer = null } -module.exports = Parser; +module.exports = JavascriptRedisParser diff --git a/lib/replyError.js b/lib/replyError.js new file mode 100644 index 0000000..e9e4c33 --- /dev/null +++ b/lib/replyError.js @@ -0,0 +1,26 @@ +'use strict' + +var util = require('util') + +function ReplyError (message) { + var limit = Error.stackTraceLimit + Error.stackTraceLimit = 2 + Error.captureStackTrace(this, this.constructor) + Error.stackTraceLimit = limit + Object.defineProperty(this, 'name', { + value: 'ReplyError', + configurable: false, + enumerable: false, + writable: true + }) + Object.defineProperty(this, 'message', { + value: message || '', + configurable: false, + enumerable: false, + writable: true + }) +} + +util.inherits(ReplyError, Error) + +module.exports = ReplyError diff --git a/package.json b/package.json index 26babb8..003e981 100644 --- a/package.json +++ b/package.json @@ -4,8 +4,9 @@ "description": "Javascript Redis protocol (RESP) parser", "main": "index.js", "scripts": { - "test": "mocha", - "posttest": "jshint . && npm run coverage && npm run coverage:check", + "test": "npm run coverage", + "benchmark": "node ./benchmark", + "posttest": "standard && npm run coverage:check", "coverage": "node ./node_modules/istanbul/lib/cli.js cover --preserve-comments ./node_modules/mocha/bin/_mocha -- -R spec", "coverage:check": "node ./node_modules/istanbul/lib/cli.js check-coverage --branch 100 --statement 100" }, @@ -28,13 +29,12 @@ "node": ">=0.10.0" }, "devDependencies": { - "codeclimate-test-reporter": "^0.1.1", + "benchmark": "^2.1.0", + "codeclimate-test-reporter": "^0.3.1", "intercept-stdout": "^0.1.2", "istanbul": "^0.4.0", - "jshint": "^2.8.0", - "mocha": "^2.3.2" - }, - "optionalDependency": { + "standard": "^7.0.1", + "mocha": "^2.3.2", "hiredis": "^0.4.1" }, "author": "Ruben Bridgewater", diff --git a/test/parsers.spec.js b/test/parsers.spec.js index 4a5a486..de81258 100644 --- a/test/parsers.spec.js +++ b/test/parsers.spec.js @@ -1,484 +1,661 @@ -'use strict'; +'use strict' -var intercept = require('intercept-stdout'); -var assert = require('assert'); -var Parser = require('../'); -var parsers = ['javascript', 'hiredis']; +/* eslint-env mocha */ + +var assert = require('assert') +var JavascriptParser = require('../') +var HiredisParser = require('../lib/hiredis') +var ReplyError = JavascriptParser.ReplyError +var parsers = [JavascriptParser, HiredisParser] // Mock the not needed return functions -function returnReply () { throw new Error('failed'); } -function returnError () { throw new Error('failed'); } -function returnFatalError () { throw new Error('failed'); } +function returnReply () { throw new Error('failed') } +function returnError () { throw new Error('failed') } +function returnFatalError () { throw new Error('failed') } describe('parsers', function () { - - describe('general parser functionality', function () { - - it('use default values', function () { - var parser = new Parser({ - returnReply: returnReply, - returnError: returnError - }); - assert.strictEqual(parser.returnError, parser.returnFatalError); - assert.strictEqual(parser.name, 'hiredis'); - }); - - it('auto parser', function () { - var parser = new Parser({ - returnReply: returnReply, - returnError: returnError, - name: 'auto' - }); - assert.strictEqual(parser.name, 'hiredis'); - }); - - it('auto parser v2', function () { - var parser = new Parser({ - returnReply: returnReply, - returnError: returnError, - name: null - }); - assert.strictEqual(parser.name, 'hiredis'); - }); - - it('fail for missing options', function () { - assert.throws(function() { - new Parser({ - returnReply: returnReply, - returnBuffers: true - }); - }, function (err) { - assert.strictEqual(err.message, 'Please provide all return functions while initiating the parser'); - return true; - }); - - }); - - it('unknown parser', function () { - var str = ''; - var unhookIntercept = intercept(function (data) { - str += data; - return ''; - }); - var parser = new Parser({ - returnReply: returnReply, - returnError: returnError, - name: 'something_unknown' - }); - unhookIntercept(); - assert.strictEqual(parser.name, 'hiredis'); - assert(/^Warning: The requested parser "something_unknown" is unkown and the default parser is choosen instead\.\n +at new Parser/.test(str), str); - }); - - it('hiredis and stringNumbers', function () { - var str = ''; - var unhookIntercept = intercept(function (data) { - str += data; - return ''; - }); - var parser = new Parser({ - returnReply: returnReply, - returnError: returnError, - name: 'hiredis', - stringNumbers: true - }); - unhookIntercept(); - assert.strictEqual(parser.name, 'javascript'); - assert(/^Warning: You explicitly required the hiredis parser in combination with the stringNumbers option. .+.\.\n +at new Parser/.test(str), str); - }); - - }); - - parsers.forEach(function (parserName) { - - describe(parserName, function () { - - it('handles multi-bulk reply and check context binding', function () { - var replyCount = 0; - function Abc () {} - Abc.prototype.checkReply = function (reply) { - assert.strictEqual(typeof this.log, 'function'); - assert.deepEqual(reply, [['a']], 'Expecting multi-bulk reply of [["a"]]'); - replyCount++; - }; - Abc.prototype.log = console.log; - var test = new Abc(); - var parser = new Parser({ - returnReply: function (reply) { - test.checkReply(reply); - }, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer('*1\r\n*1\r\n$1\r\na\r\n')); - assert.strictEqual(replyCount, 1); - - parser.execute(new Buffer('*1\r\n*1\r')); - parser.execute(new Buffer('\n$1\r\na\r\n')); - assert.strictEqual(replyCount, 2); - - parser.execute(new Buffer('*1\r\n*1\r\n')); - parser.execute(new Buffer('$1\r\na\r\n')); - - assert.equal(replyCount, 3, 'check reply should have been called three times'); - }); - - it('parser error', function () { - var replyCount = 0; - function Abc () {} - Abc.prototype.checkReply = function (reply) { - assert.strictEqual(typeof this.log, 'function'); - assert.strictEqual(reply.message, 'Protocol error, got "a" as reply type byte'); - replyCount++; - }; - Abc.prototype.log = console.log; - var test = new Abc(); - var parser = new Parser({ - returnReply: returnReply, - returnError: returnError, - returnFatalError: function (err) { - test.checkReply(err); - }, - name: parserName - }); - - parser.execute(new Buffer('a*1\r*1\r$1`zasd\r\na')); - assert.equal(replyCount, 1); - }); - - it('parser error resets the buffer', function () { - var replyCount = 0; - var errCount = 0; - function checkReply (reply) { - assert.strictEqual(reply.length, 1); - assert(Buffer.isBuffer(reply[0])); - assert.strictEqual(reply[0].toString(), 'CCC'); - replyCount++; - } - function checkError (err) { - assert.strictEqual(err.message, 'Protocol error, got "b" as reply type byte'); - errCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: checkError, - name: parserName, - returnBuffers: true - }); - - // The chunk contains valid data after the protocol error - parser.execute(new Buffer('*1\r\n+CCC\r\nb$1\r\nz\r\n+abc\r\n')); - assert.strictEqual(replyCount, 1); - assert.strictEqual(errCount, 1); - parser.execute(new Buffer('*1\r\n+CCC\r\n')); - assert.strictEqual(replyCount, 2); - }); - - it('parser error v3 without returnFatalError specified', function () { - var replyCount = 0; - var errCount = 0; - function checkReply (reply) { - assert.strictEqual(reply[0], 'OK'); - replyCount++; - } - function checkError (err) { - assert.strictEqual(err.message, 'Protocol error, got "\\n" as reply type byte'); - errCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: checkError, - name: parserName - }); - - parser.execute(new Buffer('*1\r\n+OK\r\n\n+zasd\r\n')); - assert.strictEqual(replyCount, 1); - assert.strictEqual(errCount, 1); - }); - - it('should handle \\r and \\n characters properly', function () { - // If a string contains \r or \n characters it will always be send as a bulk string - var replyCount = 0; - var entries = ['foo\r', 'foo\r\nbar', '\r\nfoo', 'foo\r\n']; - function checkReply (reply) { - assert.strictEqual(reply, entries[replyCount]); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer('$4\r\nfoo\r\r\n$8\r\nfoo\r\nbar\r\n$5\r\n\r\n')); - assert.strictEqual(replyCount, 2); - parser.execute(new Buffer('foo\r\n$5\r\nfoo\r\n\r\n')); - assert.strictEqual(replyCount, 4); - }); - - it('line breaks in the beginning of the last chunk', function () { - var replyCount = 0; - function checkReply(reply) { - assert.deepEqual(reply, [['a']], 'Expecting multi-bulk reply of [["a"]]'); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer('*1\r\n*1\r\n$1\r\na')); - assert.equal(replyCount, 0); - - parser.execute(new Buffer('\r\n*1\r\n*1\r')); - assert.equal(replyCount, 1); - parser.execute(new Buffer('\n$1\r\na\r\n*1\r\n*1\r\n$1\r\na\r\n')); - - assert.equal(replyCount, 3, 'check reply should have been called three times'); - }); - - it('multiple chunks in a bulk string', function () { - var replyCount = 0; - function checkReply(reply) { - assert.strictEqual(reply, 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij'); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer('$100\r\nabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - assert.strictEqual(replyCount, 0); - parser.execute(new Buffer('\r\n')); - assert.strictEqual(replyCount, 1); - - parser.execute(new Buffer('$100\r')); - parser.execute(new Buffer('\nabcdefghijabcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghij')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer( - 'abcdefghij\r\n' + - '$100\r\nabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij\r\n' + - '$100\r\nabcdefghijabcdefghijabcdefghijabcdefghij' - )); - assert.strictEqual(replyCount, 3); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij\r')); - assert.strictEqual(replyCount, 3); - parser.execute(new Buffer('\n')); - - assert.equal(replyCount, 4, 'check reply should have been called three times'); - }); - - it('multiple chunks with arrays different types', function () { - var replyCount = 0; - var predefined_data = [ - 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij', - 'test', - 100, - new Error('Error message'), - ['The force awakens'] - ]; - function checkReply(reply) { - for (var i = 0; i < reply.length; i++) { - if (i < 3) { - assert.strictEqual(reply[i], predefined_data[i]); - } else { - assert.deepEqual(reply[i], predefined_data[i]); - } - } - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName, - returnBuffers: false - }); - - parser.execute(new Buffer('*5\r\n$100\r\nabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')); - parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij\r\n')); - parser.execute(new Buffer('+test\r')); - parser.execute(new Buffer('\n:100')); - parser.execute(new Buffer('\r\n-Error message')); - parser.execute(new Buffer('\r\n*1\r\n$17\r\nThe force')); - assert.strictEqual(replyCount, 0); - parser.execute(new Buffer(' awakens\r\n$5')); - assert.strictEqual(replyCount, 1); - }); - - it('return normal errors', function () { - var replyCount = 0; - function checkReply(reply) { - assert.equal(reply.message, 'Error message'); - replyCount++; - } - var parser = new Parser({ - returnReply: returnError, - returnError: checkReply, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer('-Error ')); - parser.execute(new Buffer('message\r\n*3\r\n$17\r\nThe force')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer(' awakens\r\n$5')); - assert.strictEqual(replyCount, 1); - }); - - it('return null for empty arrays and empty bulk strings', function () { - var replyCount = 0; - function checkReply(reply) { - assert.equal(reply, null); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer('$-1\r\n*-')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer('1')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer('\r\n$-')); - assert.strictEqual(replyCount, 2); - }); - - it('return value even if all chunks are only 1 character long', function () { - var replyCount = 0; - function checkReply(reply) { - assert.equal(reply, 1); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer(':')); - assert.strictEqual(replyCount, 0); - parser.execute(new Buffer('1')); - assert.strictEqual(replyCount, 0); - parser.execute(new Buffer('\r')); - assert.strictEqual(replyCount, 0); - parser.execute(new Buffer('\n')); - assert.strictEqual(replyCount, 1); - }); - - it('do not return before \\r\\n', function () { - var replyCount = 0; - function checkReply(reply) { - assert.equal(reply, 1); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - - parser.execute(new Buffer(':1\r\n:')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer('1')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer('\r')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer('\n')); - assert.strictEqual(replyCount, 2); - }); - - it('return data as buffer if requested', function () { - var replyCount = 0; - function checkReply(reply) { - if (Array.isArray(reply)) { - reply = reply[0]; - } - assert(Buffer.isBuffer(reply)); - assert.strictEqual(reply.inspect(), new Buffer('test').inspect()); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName, - returnBuffers: true - }); - - parser.execute(new Buffer('+test\r\n')); - assert.strictEqual(replyCount, 1); - parser.execute(new Buffer('$4\r\ntest\r\n')); - assert.strictEqual(replyCount, 2); - parser.execute(new Buffer('*1\r\n$4\r\ntest\r\n')); - assert.strictEqual(replyCount, 3); - }); - - it('handle special case buffer sizes properly', function () { - var replyCount = 0; - var entries = ['test test ', 'test test test test ', 1234]; - function checkReply(reply) { - assert.strictEqual(reply, entries[replyCount]); - replyCount++; - } - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName - }); - parser.execute(new Buffer('$10\r\ntest ')); - assert.strictEqual(replyCount, 0); - parser.execute(new Buffer('test \r\n$20\r\ntest test test test \r\n:1234\r')); - assert.strictEqual(replyCount, 2); - parser.execute(new Buffer('\n')); - assert.strictEqual(replyCount, 3); - }); - - it('return numbers as strings', function () { - var replyCount = 0; - var entries = ['123', '590295810358705700002', '-99999999999999999']; - function checkReply(reply) { - assert.strictEqual(typeof reply, 'string'); - assert.strictEqual(reply, entries[replyCount]); - replyCount++; - } - var unhookIntercept = intercept(function () { - return ''; - }); - var parser = new Parser({ - returnReply: checkReply, - returnError: returnError, - returnFatalError: returnFatalError, - name: parserName, - stringNumbers: true - }); - unhookIntercept(); - parser.execute(new Buffer(':123\r\n:590295810358705700002\r\n:-99999999999999999\r\n')); - assert.strictEqual(replyCount, 3); - }); - }); - }); -}); + describe('general parser functionality', function () { + it('backwards compatibility with hiredis', function () { + var parser = new JavascriptParser({ + returnReply: returnReply, + returnError: returnError, + name: 'hiredis' + }) + assert.strictEqual(parser.name, 'hiredis') + }) + + it('fail for missing options argument', function () { + assert.throws(function () { + JavascriptParser() + }, function (err) { + assert.strictEqual(err.message, 'Please provide all return functions while initiating the parser') + assert(err instanceof TypeError) + return true + }) + }) + + it('fail for faulty options properties', function () { + assert.throws(function () { + JavascriptParser({ + returnReply: returnReply, + returnError: true + }) + }, function (err) { + assert.strictEqual(err.message, 'The options argument contains the property "returnError" that is either unkown or of a wrong type') + assert(err instanceof TypeError) + return true + }) + }) + + it('fail for faulty options properties #2', function () { + assert.throws(function () { + JavascriptParser({ + returnReply: returnReply, + returnError: returnError, + bla: undefined + }) + }, function (err) { + assert.strictEqual(err.message, 'The options argument contains the property "bla" that is either unkown or of a wrong type') + assert(err instanceof TypeError) + return true + }) + }) + }) + + parsers.forEach(function (Parser) { + describe(Parser.name, function () { + it('chunks getting to big for the bufferPool', function () { + // This is a edge case. Chunks should not exceed Math.pow(2, 16) bytes + var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + + 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars + var bigString = (new Array(Math.pow(2, 17) / lorem.length + 1).join(lorem)) // Math.pow(2, 17) chars long + var replyCount = 0 + var sizes = [4, Math.pow(2, 17)] + function checkReply (reply) { + assert.strictEqual(sizes[replyCount], reply.length) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + parser.execute(new Buffer('+test')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('\r\n+')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer(bigString)) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 2) + }) + + it('handles multi-bulk reply and check context binding', function () { + var replyCount = 0 + function Abc () {} + Abc.prototype.checkReply = function (reply) { + assert.strictEqual(typeof this.log, 'function') + assert.deepEqual(reply, [['a']], 'Expecting multi-bulk reply of [["a"]]') + replyCount++ + } + Abc.prototype.log = console.log + var test = new Abc() + var parser = new Parser({ + returnReply: function (reply) { + test.checkReply(reply) + }, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer('*1\r\n*1\r\n$1\r\na\r\n')) + assert.strictEqual(replyCount, 1) + + parser.execute(new Buffer('*1\r\n*1\r')) + parser.execute(new Buffer('\n$1\r\na\r\n')) + assert.strictEqual(replyCount, 2) + + parser.execute(new Buffer('*1\r\n*1\r\n')) + parser.execute(new Buffer('$1\r\na\r\n')) + + assert.equal(replyCount, 3, 'check reply should have been called three times') + }) + + it('parser error', function () { + var replyCount = 0 + function Abc () {} + Abc.prototype.checkReply = function (err) { + assert.strictEqual(typeof this.log, 'function') + assert.strictEqual(err.message, 'Protocol error, got "a" as reply type byte') + assert.strictEqual(err.name, 'ReplyError') + assert(err instanceof ReplyError) + assert(err instanceof Error) + replyCount++ + } + Abc.prototype.log = console.log + var test = new Abc() + var parser = new Parser({ + returnReply: returnReply, + returnError: returnError, + returnFatalError: function (err) { + test.checkReply(err) + } + }) + + parser.execute(new Buffer('a*1\r*1\r$1`zasd\r\na')) + assert.equal(replyCount, 1) + }) + + it('parser error resets the buffer', function () { + var replyCount = 0 + var errCount = 0 + function checkReply (reply) { + assert.strictEqual(reply.length, 1) + assert(Buffer.isBuffer(reply[0])) + assert.strictEqual(reply[0].toString(), 'CCC') + replyCount++ + } + function checkError (err) { + assert.strictEqual(err.message, 'Protocol error, got "b" as reply type byte') + errCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: checkError, + returnFatalError: checkError, + returnBuffers: true + }) + + // The chunk contains valid data after the protocol error + parser.execute(new Buffer('*1\r\n+CCC\r\nb$1\r\nz\r\n+abc\r\n')) + assert.strictEqual(replyCount, 1) + assert.strictEqual(errCount, 1) + parser.execute(new Buffer('*1\r\n+CCC\r\n')) + assert.strictEqual(replyCount, 2) + parser.execute(new Buffer('-Protocol error, got "b" as reply type byte\r\n')) + assert.strictEqual(errCount, 2) + }) + + it('parser error v3 without returnFatalError specified', function () { + var replyCount = 0 + var errCount = 0 + function checkReply (reply) { + assert.strictEqual(reply[0], 'OK') + replyCount++ + } + function checkError (err) { + assert.strictEqual(err.message, 'Protocol error, got "\\n" as reply type byte') + errCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: checkError + }) + + parser.execute(new Buffer('*1\r\n+OK\r\n\n+zasd\r\n')) + assert.strictEqual(replyCount, 1) + assert.strictEqual(errCount, 1) + }) + + it('should handle \\r and \\n characters properly', function () { + // If a string contains \r or \n characters it will always be send as a bulk string + var replyCount = 0 + var entries = ['foo\r', 'foo\r\nbar', '\r\nfoo', 'foo\r\n', 'foo', 'foobar', 'foo\r', 'äfooöü', 'abc'] + function checkReply (reply) { + assert.strictEqual(reply, entries[replyCount]) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer('$4\r\nfoo\r\r\n$8\r\nfoo\r\nbar\r\n$5\r\n\r\n')) + assert.strictEqual(replyCount, 2) + parser.execute(new Buffer('foo\r\n$5\r\nfoo\r\n\r\n')) + assert.strictEqual(replyCount, 4) + parser.execute(new Buffer('+foo\r')) + assert.strictEqual(replyCount, 4) + parser.execute(new Buffer('\n$6\r\nfoobar\r')) + assert.strictEqual(replyCount, 5) + parser.execute(new Buffer('\n$4\r\nfoo\r\r\n')) + assert.strictEqual(replyCount, 7) + parser.execute(new Buffer('$9\r\näfo')) + parser.execute(new Buffer('oö')) + parser.execute(new Buffer('ü\r')) + assert.strictEqual(replyCount, 7) + parser.execute(new Buffer('\n+abc\r\n')) + assert.strictEqual(replyCount, 9) + }) + + it('line breaks in the beginning of the last chunk', function () { + var replyCount = 0 + function checkReply (reply) { + assert.deepEqual(reply, [['a']], 'Expecting multi-bulk reply of [["a"]]') + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer('*1\r\n*1\r\n$1\r\na')) + assert.equal(replyCount, 0) + + parser.execute(new Buffer('\r\n*1\r\n*1\r')) + assert.equal(replyCount, 1) + parser.execute(new Buffer('\n$1\r\na\r\n*1\r\n*1\r\n$1\r\na\r\n')) + + assert.equal(replyCount, 3, 'check reply should have been called three times') + }) + + it('multiple chunks in a bulk string', function () { + var replyCount = 0 + function checkReply (reply) { + assert.strictEqual(reply, 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij') + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer('$100\r\nabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 1) + + parser.execute(new Buffer('$100\r')) + parser.execute(new Buffer('\nabcdefghijabcdefghijabcdefghijabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghij')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer( + 'abcdefghij\r\n' + + '$100\r\nabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij\r\n' + + '$100\r\nabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij' + )) + assert.strictEqual(replyCount, 3) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij\r')) + assert.strictEqual(replyCount, 3) + parser.execute(new Buffer('\n')) + + assert.equal(replyCount, 4, 'check reply should have been called three times') + }) + + it('multiple chunks with arrays different types', function () { + var replyCount = 0 + var predefinedData = [ + 'abcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghijabcdefghij', + 'test', + 100, + new ReplyError('Error message'), + ['The force awakens'], + new ReplyError() + ] + function checkReply (reply) { + for (var i = 0; i < reply.length; i++) { + if (Array.isArray(reply[i])) { + reply[i].forEach(function (reply, j) { + assert.strictEqual(reply, predefinedData[i][j]) + }) + } else if (reply[i] instanceof Error) { + if (Parser.name !== 'HiredisReplyParser') { // The hiredis always returns normal errors in case of nested ones + assert(reply[i] instanceof ReplyError) + assert.strictEqual(reply[i].name, predefinedData[i].name) + } + assert.strictEqual(reply[i].message, predefinedData[i].message) + } else { + assert.strictEqual(reply[i], predefinedData[i]) + } + } + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError, + returnBuffers: false + }) + + parser.execute(new Buffer('*6\r\n$100\r\nabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij')) + parser.execute(new Buffer('abcdefghijabcdefghijabcdefghij\r\n')) + parser.execute(new Buffer('+test\r')) + parser.execute(new Buffer('\n:100')) + parser.execute(new Buffer('\r\n-Error message')) + parser.execute(new Buffer('\r\n*1\r\n$17\r\nThe force')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer(' awakens\r\n-\r\n$5')) + assert.strictEqual(replyCount, 1) + }) + + it('return normal errors', function () { + var replyCount = 0 + function checkReply (reply) { + assert.equal(reply.message, 'Error message') + replyCount++ + } + var parser = new Parser({ + returnReply: returnError, + returnError: checkReply, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer('-Error ')) + parser.execute(new Buffer('message\r\n*3\r\n$17\r\nThe force')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer(' awakens\r\n$5')) + assert.strictEqual(replyCount, 1) + }) + + it('return null for empty arrays and empty bulk strings', function () { + var replyCount = 0 + function checkReply (reply) { + assert.equal(reply, null) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer('$-1\r\n*-')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('1')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('\r\n$-')) + assert.strictEqual(replyCount, 2) + }) + + it('return value even if all chunks are only 1 character long', function () { + var replyCount = 0 + function checkReply (reply) { + assert.equal(reply, 1) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer(':')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('1')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('\r')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('\n')) + assert.strictEqual(replyCount, 1) + }) + + it('do not return before \\r\\n', function () { + var replyCount = 0 + function checkReply (reply) { + assert.equal(reply, 1) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + + parser.execute(new Buffer(':1\r\n:')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('1')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('\r')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('\n')) + assert.strictEqual(replyCount, 2) + }) + + it('return data as buffer if requested', function () { + var replyCount = 0 + function checkReply (reply) { + if (Array.isArray(reply)) { + reply = reply[0] + } + assert(Buffer.isBuffer(reply)) + assert.strictEqual(reply.inspect(), new Buffer('test').inspect()) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError, + returnBuffers: true + }) + + parser.execute(new Buffer('+test\r\n')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('$4\r\ntest\r\n')) + assert.strictEqual(replyCount, 2) + parser.execute(new Buffer('*1\r\n$4\r\ntest\r\n')) + assert.strictEqual(replyCount, 3) + }) + + it('handle special case buffer sizes properly', function () { + var replyCount = 0 + var entries = ['test test ', 'test test test test ', 1234] + function checkReply (reply) { + assert.strictEqual(reply, entries[replyCount]) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + parser.execute(new Buffer('$10\r\ntest ')) + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('test \r\n$20\r\ntest test test test \r\n:1234\r')) + assert.strictEqual(replyCount, 2) + parser.execute(new Buffer('\n')) + assert.strictEqual(replyCount, 3) + }) + + it('return numbers as strings', function () { + if (Parser.name === 'HiredisReplyParser') { + return this.skip() + } + var replyCount = 0 + var entries = ['123', '590295810358705700002', '-99999999999999999'] + function checkReply (reply) { + assert.strictEqual(typeof reply, 'string') + assert.strictEqual(reply, entries[replyCount]) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError, + stringNumbers: true + }) + parser.execute(new Buffer(':123\r\n:590295810358705700002\r\n:-99999999999999999\r\n')) + assert.strictEqual(replyCount, 3) + }) + + it('handle big numbers', function () { + var replyCount = 0 + var number = 9007199254740991 // Number.MAX_SAFE_INTEGER + function checkReply (reply) { + assert.strictEqual(reply, number++) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + parser.execute(new Buffer(':' + number + '\r\n')) + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer(':' + number + '\r\n')) + assert.strictEqual(replyCount, 2) + }) + + it('handle big data with buffers', function (done) { + var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + + 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars + var bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long + var startBigBuffer = new Buffer('\r\n$' + (128 * 1024) + '\r\n') + var startSecondBigBuffer = new Buffer('\r\n$' + (256 * 1024) + '\r\n') + var chunks = new Array(4) + for (var i = 0; i < 4; i++) { + chunks[i] = new Buffer(bigStringArray.join(' ') + '.') // Math.pow(2, 16) chars long + } + var replyCount = 0 + function checkReply (reply) { + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError, + returnBuffers: true + }) + parser.execute(new Buffer('+test')) + assert.strictEqual(replyCount, 0) + parser.execute(startBigBuffer) + for (i = 0; i < 2; i++) { + if (Parser.name === 'JavascriptReplyParser') { + assert.strictEqual(parser.bufferCache.length, i + 1) + } + parser.execute(chunks[i]) + } + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 2) + parser.execute(new Buffer('+test')) + assert.strictEqual(replyCount, 2) + parser.execute(startSecondBigBuffer) + for (i = 0; i < 4; i++) { + if (Parser.name === 'JavascriptReplyParser') { + assert.strictEqual(parser.bufferCache.length, i + 1) + } + parser.execute(chunks[i]) + } + assert.strictEqual(replyCount, 3) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 4) + // Delay done so the bufferPool is cleared and tested + // If the buffer is not cleared, the coverage is not going to be at 100% + setTimeout(done, 600) + }) + + it('handle big data', function () { + var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + + 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars + var bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long + var startBigBuffer = new Buffer('$' + (4 * 1024 * 1024) + '\r\n') + var chunks = new Array(64) + for (var i = 0; i < 64; i++) { + chunks[i] = new Buffer(bigStringArray.join(' ') + '.') // Math.pow(2, 16) chars long + } + var replyCount = 0 + function checkReply (reply) { + assert.strictEqual(reply.length, 4 * 1024 * 1024) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + parser.execute(startBigBuffer) + for (i = 0; i < 64; i++) { + if (Parser.name === 'JavascriptReplyParser') { + assert.strictEqual(parser.bufferCache.length, i + 1) + } + parser.execute(chunks[i]) + } + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 1) + }) + + it('handle big data 2', function () { + var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + + 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars + var bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long + var startBigBuffer = new Buffer('\r\n$' + (4 * 1024 * 1024) + '\r\n') + var chunks = new Array(64) + for (var i = 0; i < 64; i++) { + chunks[i] = new Buffer(bigStringArray.join(' ') + '.') // Math.pow(2, 16) chars long + } + var replyCount = 0 + function checkReply (reply) { + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError + }) + parser.execute(new Buffer('+test')) + parser.execute(startBigBuffer) + for (i = 0; i < 64; i++) { + if (Parser.name === 'JavascriptReplyParser') { + assert.strictEqual(parser.bufferCache.length, i + 1) + } + parser.execute(chunks[i]) + } + assert.strictEqual(replyCount, 1) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 2) + }) + + it('handle big data 2 with buffers', function () { + var lorem = 'Lorem ipsum dolor sit amet, consectetur adipiscing elit, ' + + 'sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. ' + + 'Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ' + + 'ut aliquip ex ea commodo consequat. Duis aute irure dolor in' // 256 chars + var bigStringArray = (new Array(Math.pow(2, 16) / lorem.length).join(lorem + ' ')).split(' ') // Math.pow(2, 16) chars long + var startBigBuffer = new Buffer('$' + (4 * 1024 * 1024) + '\r\n') + var chunks = new Array(64) + for (var i = 0; i < 64; i++) { + chunks[i] = new Buffer(bigStringArray.join(' ') + '.') // Math.pow(2, 16) chars long + } + var replyCount = 0 + function checkReply (reply) { + assert.strictEqual(reply.length, 4 * 1024 * 1024) + replyCount++ + } + var parser = new Parser({ + returnReply: checkReply, + returnError: returnError, + returnFatalError: returnFatalError, + returnBuffers: true + }) + parser.execute(startBigBuffer) + for (i = 0; i < 64; i++) { + if (Parser.name === 'JavascriptReplyParser') { + assert.strictEqual(parser.bufferCache.length, i + 1) + } + parser.execute(chunks[i]) + } + assert.strictEqual(replyCount, 0) + parser.execute(new Buffer('\r\n')) + assert.strictEqual(replyCount, 1) + }) + }) + }) +})